mcp-google-multi 6.0.1 → 6.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,8 +4,8 @@ import { drive as driveClient } from '@googleapis/drive';
4
4
  import { accountArgLive } from '../accounts.js';
5
5
  import { getClient } from '../client.js';
6
6
  import { handleGoogleApiError, invalidParams, safeMessage, stringifyEnvelope } from './_errors.js';
7
- import { hostFilesRefused, openLocalReadStream, prepareLocalDest } from './_local-files.js';
8
- import { checkOutbound, outboundDeniedEnvelope, resolveOutboundAllowlist } from '../outbound-allowlist.js';
7
+ import { hostFilesRefused, openLocalReadStream, openInternalReadStream, prepareLocalDest, cleanupPartialWrite, localFileErrorResult, LocalFileError } from './_local-files.js';
8
+ import { checkOutbound, checkOutboundGrantee, driveGranteeFromAccessProposal, driveGranteeFromPermission, outboundDeniedEnvelope, resolveOutboundAllowlist } from '../outbound-allowlist.js';
9
9
  import { isAllowed, writeDisabledResult } from '../write-control.js';
10
10
  import { capText, listResult } from '../trim.js';
11
11
  import * as fs from 'fs';
@@ -351,7 +351,7 @@ export function registerDriveTools(server, deps = {}) {
351
351
  contentBase64: z.string().optional().describe(`Base64-encoded file content. Exactly one of contentBase64${localFiles ? ' or localPath' : ''} is required. Decoded size is capped at ${MAX_UPLOAD_BASE64_BYTES / (1024 * 1024)} MB.`),
352
352
  ...(localFiles
353
353
  ? {
354
- localPath: z.string().min(1).optional().describe('Absolute path of the SOURCE file on disk to upload (on the machine running the server; this is not savePath). Exactly one of contentBase64 or localPath is required.'),
354
+ localPath: z.string().min(1).optional().describe('Path of the SOURCE file to upload, on the machine running the server (this is not savePath): confined to GOOGLE_LOCAL_FILES_DIR (default <home>/Downloads/mcp-google-multi); a relative path resolves inside it, an absolute path must already be inside it. Exactly one of contentBase64 or localPath is required.'),
355
355
  }
356
356
  : {}),
357
357
  mimeType: z.string().optional().describe('Source MIME type of the content (inferred from filename if omitted). With `convertTo`, this is the format Drive imports from.'),
@@ -408,6 +408,8 @@ export function registerDriveTools(server, deps = {}) {
408
408
  };
409
409
  }
410
410
  catch (error) {
411
+ if (error instanceof LocalFileError)
412
+ return localFileErrorResult(error, account);
411
413
  return handleDriveError(error, account);
412
414
  }
413
415
  });
@@ -416,7 +418,7 @@ export function registerDriveTools(server, deps = {}) {
416
418
  inputSchema: {
417
419
  account: accountEnum.describe('Google account alias'),
418
420
  fileId: z.string().min(1).describe('Google Drive file ID'),
419
- savePath: z.string().min(1).describe('Absolute DIRECTORY path to save into (created if missing, on the machine running the server); the file name comes from `filename`'),
421
+ savePath: z.string().min(1).describe('Destination folder to save into, on the machine running the server: confined to GOOGLE_LOCAL_FILES_DIR (default <home>/Downloads/mcp-google-multi), created if missing; a relative path resolves inside it, an absolute path must already be inside it. The file name comes from `filename`.'),
420
422
  filename: z.string().optional().describe('Filename to save as (defaults to the file name in Drive)'),
421
423
  },
422
424
  }, async ({ account, fileId, savePath, filename }) => {
@@ -426,16 +428,31 @@ export function registerDriveTools(server, deps = {}) {
426
428
  const name = filename
427
429
  ?? (await drive.files.get({ fileId, fields: 'name', supportsAllDrives: true })).data.name
428
430
  ?? fileId;
429
- const dest = prepareLocalDest(savePath, name);
431
+ const dest = await prepareLocalDest(savePath, name);
430
432
  const res = await drive.files.get({ fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'stream' });
431
- // pipeline destroys both streams on source/sink error; raw .pipe leaks the partial file.
432
- await pipeline(res.data, fs.createWriteStream(dest, { mode: 0o600 }));
433
+ // pipeline destroys both streams on source/sink error; raw .pipe leaks the
434
+ // partial file. wx: prepareLocalDest already checked existence, this is
435
+ // the belt-and-suspenders race guard, never silently clobber.
436
+ try {
437
+ await pipeline(res.data, fs.createWriteStream(dest, { mode: 0o600, flags: 'wx' }));
438
+ }
439
+ catch (err) {
440
+ // A failed download must not leave a partial file behind blocking
441
+ // a retry with local_path_exists; best-effort, original error wins.
442
+ // EEXIST means wx itself refused to open: the TOCTOU race lost to
443
+ // another call that already owns dest, so this call never wrote
444
+ // anything there and must not delete that other call's file.
445
+ await cleanupPartialWrite(dest, err);
446
+ throw err;
447
+ }
433
448
  const { size } = fs.statSync(dest);
434
449
  return {
435
450
  content: [{ type: 'text', text: JSON.stringify({ savedPath: dest, bytes: size }, null, 2) }],
436
451
  };
437
452
  }
438
453
  catch (error) {
454
+ if (error instanceof LocalFileError)
455
+ return localFileErrorResult(error, account);
439
456
  return handleDriveError(error, account);
440
457
  }
441
458
  });
@@ -445,7 +462,7 @@ export function registerDriveTools(server, deps = {}) {
445
462
  account: accountEnum.describe('Google account alias'),
446
463
  fileId: z.string().min(1).describe('Google Drive file ID'),
447
464
  mimeType: z.string().describe('Target export MIME type (e.g. "application/pdf", "text/markdown", "application/vnd.openxmlformats-officedocument.wordprocessingml.document")'),
448
- savePath: z.string().min(1).describe('Absolute DIRECTORY path to save into (created if missing, on the machine running the server); the file name comes from `filename`'),
465
+ savePath: z.string().min(1).describe('Destination folder to save into, on the machine running the server: confined to GOOGLE_LOCAL_FILES_DIR (default <home>/Downloads/mcp-google-multi), created if missing; a relative path resolves inside it, an absolute path must already be inside it. The file name comes from `filename`.'),
449
466
  filename: z.string().optional().describe('Filename to save as (defaults to the Drive name plus the extension implied by mimeType)'),
450
467
  },
451
468
  }, async ({ account, fileId, mimeType: exportMime, savePath, filename }) => {
@@ -458,16 +475,31 @@ export function registerDriveTools(server, deps = {}) {
458
475
  const ext = mime.extension(exportMime);
459
476
  name = `${meta.data.name ?? fileId}${ext ? `.${ext}` : ''}`;
460
477
  }
461
- const dest = prepareLocalDest(savePath, name);
478
+ const dest = await prepareLocalDest(savePath, name);
462
479
  const res = await drive.files.export({ fileId, mimeType: exportMime }, { responseType: 'stream' });
463
- // pipeline destroys both streams on source/sink error; raw .pipe leaks the partial file.
464
- await pipeline(res.data, fs.createWriteStream(dest, { mode: 0o600 }));
480
+ // pipeline destroys both streams on source/sink error; raw .pipe leaks the
481
+ // partial file. wx: prepareLocalDest already checked existence, this is
482
+ // the belt-and-suspenders race guard, never silently clobber.
483
+ try {
484
+ await pipeline(res.data, fs.createWriteStream(dest, { mode: 0o600, flags: 'wx' }));
485
+ }
486
+ catch (err) {
487
+ // A failed download must not leave a partial file behind blocking
488
+ // a retry with local_path_exists; best-effort, original error wins.
489
+ // EEXIST means wx itself refused to open: the TOCTOU race lost to
490
+ // another call that already owns dest, so this call never wrote
491
+ // anything there and must not delete that other call's file.
492
+ await cleanupPartialWrite(dest, err);
493
+ throw err;
494
+ }
465
495
  const { size } = fs.statSync(dest);
466
496
  return {
467
497
  content: [{ type: 'text', text: JSON.stringify({ savedPath: dest, bytes: size }, null, 2) }],
468
498
  };
469
499
  }
470
500
  catch (error) {
501
+ if (error instanceof LocalFileError)
502
+ return localFileErrorResult(error, account);
471
503
  return handleDriveError(error, account);
472
504
  }
473
505
  });
@@ -508,7 +540,7 @@ export function registerDriveTools(server, deps = {}) {
508
540
  newParentFolderId: z.string().optional().describe('Move to this folder, named newParentFolderId here, not parentFolderId'),
509
541
  ...(localFiles
510
542
  ? {
511
- localPath: z.string().optional().describe('Replace file content with this local file (path on the machine running the server)'),
543
+ localPath: z.string().optional().describe('Replace file content with this local file, on the machine running the server: confined to GOOGLE_LOCAL_FILES_DIR (default <home>/Downloads/mcp-google-multi); a relative path resolves inside it, an absolute path must already be inside it.'),
512
544
  mimeType: z.string().optional().describe('MIME type of the replacement file (required if localPath is provided)'),
513
545
  convertTo: z.enum(CONVERT_TO_VALUES).optional().describe('When replacing content via localPath, convert the new content into this native Google Workspace type on import: "document" | "spreadsheet" | "presentation" | "drawing" (full application/vnd.google-apps.* ids also accepted).'),
514
546
  }
@@ -551,6 +583,8 @@ export function registerDriveTools(server, deps = {}) {
551
583
  };
552
584
  }
553
585
  catch (error) {
586
+ if (error instanceof LocalFileError)
587
+ return localFileErrorResult(error, account);
554
588
  return handleDriveError(error, account);
555
589
  }
556
590
  });
@@ -802,6 +836,20 @@ export function registerDriveTools(server, deps = {}) {
802
836
  try {
803
837
  const auth = await getClientFn(account);
804
838
  const drive = driveClient({ version: 'v3', auth });
839
+ // This call's own body never carries the grantee (only role/
840
+ // expiration): look the real grantee up by permissionId rather than
841
+ // trust anything the caller claims it is.
842
+ if (resolveOutboundAllowlist()) {
843
+ const current = await drive.permissions.get({
844
+ fileId,
845
+ permissionId,
846
+ supportsAllDrives: true,
847
+ fields: 'type,domain,emailAddress',
848
+ });
849
+ const outbound = checkOutboundGrantee('drive_permission_update', driveGranteeFromPermission(current.data), String(account));
850
+ if (outbound)
851
+ return outbound;
852
+ }
805
853
  const requestBody = {};
806
854
  if (role)
807
855
  requestBody.role = role;
@@ -1131,6 +1179,12 @@ export function registerDriveTools(server, deps = {}) {
1131
1179
  publishedOutsideDomain: coerceBoolean.optional().describe('Allow publish outside domain'),
1132
1180
  },
1133
1181
  }, async ({ account, fileId, revisionId, keepForever, published, publishAuto, publishedOutsideDomain }) => {
1182
+ // Publishing makes the revision reachable without an invite (the
1183
+ // allowlist cannot scope that to specific recipients the way a
1184
+ // share/grant can), so it is refused outright while the allowlist is active.
1185
+ if ((published || publishedOutsideDomain) && resolveOutboundAllowlist()) {
1186
+ return outboundDeniedEnvelope('drive_revision_update publish', ['(publishing exposes it beyond any addressable recipient)'], String(account));
1187
+ }
1134
1188
  try {
1135
1189
  const auth = await getClientFn(account);
1136
1190
  const drive = driveClient({ version: 'v3', auth });
@@ -1220,6 +1274,15 @@ export function registerDriveTools(server, deps = {}) {
1220
1274
  try {
1221
1275
  const auth = await getClientFn(account);
1222
1276
  const drive = driveClient({ version: 'v3', auth });
1277
+ // ACCEPT grants access to whoever the proposal names; DENY/other
1278
+ // actions grant nothing and need no check. The real recipient is a
1279
+ // property of the proposal itself, not of this call's own body.
1280
+ if (action === 'ACCEPT' && resolveOutboundAllowlist()) {
1281
+ const proposal = await drive.accessproposals.get({ fileId, proposalId });
1282
+ const outbound = checkOutboundGrantee('drive_access_proposal_resolve', driveGranteeFromAccessProposal(proposal.data), String(account));
1283
+ if (outbound)
1284
+ return outbound;
1285
+ }
1223
1286
  const requestBody = { action };
1224
1287
  if (role)
1225
1288
  requestBody.role = role;
@@ -1592,7 +1655,11 @@ async function downloadAndUpload(sourceDrive, targetDrive, fileId, sourceMime, p
1592
1655
  },
1593
1656
  media: {
1594
1657
  mimeType: plan.kind === 'native' ? plan.exportMime : (sourceMime ?? 'application/octet-stream'),
1595
- body: await openLocalReadStream(tmp),
1658
+ // tmp is the server's own staging file, not a caller argument: it
1659
+ // must not go through the GOOGLE_LOCAL_FILES_DIR guard (which would
1660
+ // refuse it outright once that root differs from os.tmpdir(), and
1661
+ // would create the root as an unwanted side effect even then).
1662
+ body: await openInternalReadStream(tmp),
1596
1663
  },
1597
1664
  supportsAllDrives: true,
1598
1665
  fields: 'id,name,mimeType,webViewLink',
@@ -1,5 +1,8 @@
1
1
  import { accountAliasSchema, accountAliasSchemaFor, accountArgLive } from '../../accounts.js';
2
2
  import { executeApiMethod } from '../../executor.js';
3
+ import { serviceOf } from '../../registry.js';
4
+ import { escapeHatchNames } from '../google-api.js';
5
+ import { escapeHatchGate } from '../_escape-hatch.js';
3
6
  export function accountField(aliases) {
4
7
  // Generated code passes a getter over registry.accountAliases(), so the field
5
8
  // validates against the registry's CURRENT set, as curated tools do. An
@@ -11,12 +14,17 @@ export function registerGeneratedTool(registry, def, deps = {}) {
11
14
  // Widened locally: `cud` is a registry extension the SDK config type doesn't
12
15
  // carry; the registry reads it before handing the config to the SDK.
13
16
  const register = registry.registerTool;
17
+ const service = serviceOf(def.name);
14
18
  register(def.name, {
15
19
  description: def.description,
16
20
  inputSchema: def.shape,
17
21
  cud: def.cud,
18
22
  annotations: { openWorldHint: true },
19
23
  requiredScopes: def.method.scopes,
24
+ // Every generated per-method tool IS the escape hatch (same as
25
+ // google_api_call): write-control for a write runs inside the
26
+ // handler below, not the registry's own name-based gate.
27
+ escapeHatch: true,
20
28
  }, async (args) => {
21
29
  const pathParams = {};
22
30
  const queryParams = {};
@@ -39,6 +47,20 @@ export function registerGeneratedTool(registry, def, deps = {}) {
39
47
  }
40
48
  body = assembled;
41
49
  }
50
+ if (def.cud !== 'read') {
51
+ const names = escapeHatchNames(service, def.method.id);
52
+ const gate = await escapeHatchGate(registry, names.deny, names.primary, { service, cud: def.cud, scopes: def.method.scopes }, registry.policy, {
53
+ account: args.account,
54
+ accountEmail: registry.accountSet().configs[args.account]?.email,
55
+ methodId: def.method.id,
56
+ httpMethod: def.method.httpMethod,
57
+ pathParams,
58
+ queryParams,
59
+ body,
60
+ });
61
+ if (gate)
62
+ return gate;
63
+ }
42
64
  return executeApiMethod(def.method, {
43
65
  account: args.account,
44
66
  pathParams,
@@ -128,7 +128,7 @@ export function registerAdminGeneratedTools(registry, deps = {}) {
128
128
  }, deps);
129
129
  registerGeneratedTool(registry, {
130
130
  name: "admin_customer_devices_chromeos_batch_change_status",
131
- cud: "update",
131
+ cud: "delete",
132
132
  description: "Changes the status of a batch of ChromeOS devices. For more information about changing a ChromeOS device state [Repair, repurpose, or retire ChromeOS devices](h",
133
133
  method: { id: "admin.customer.devices.chromeos.batchChangeStatus", httpMethod: "POST", path: "admin/directory/v1/customer/{customerId}/devices/chromeos:batchChangeStatus", baseUrl: "https://admin.googleapis.com/", requiredParams: ["customerId"], scopes: S_admin_directory_v1[1] },
134
134
  params: [{ "field": "customerId", "api": "customerId", "location": "path" }, { "field": "fields", "api": "fields", "location": "query" }],
@@ -176,7 +176,7 @@ export function registerAdminGeneratedTools(registry, deps = {}) {
176
176
  }, deps);
177
177
  registerGeneratedTool(registry, {
178
178
  name: "admin_customer_devices_chromeos_issue_command",
179
- cud: "create",
179
+ cud: "delete",
180
180
  description: "Issues a command for the device to execute.",
181
181
  method: { id: "admin.customer.devices.chromeos.issueCommand", httpMethod: "POST", path: "admin/directory/v1/customer/{customerId}/devices/chromeos/{deviceId}:issueCommand", baseUrl: "https://admin.googleapis.com/", requiredParams: ["customerId", "deviceId"], scopes: S_admin_directory_v1[1] },
182
182
  params: [{ "field": "customerId", "api": "customerId", "location": "path" }, { "field": "deviceId", "api": "deviceId", "location": "path" }, { "field": "fields", "api": "fields", "location": "query" }],
@@ -1,2 +1,5 @@
1
+ import type { Cud } from '../../registry.js';
1
2
  export declare const GENERATED_METHOD_TOOLS: Record<string, string>;
2
3
  export declare const CURATED_METHOD_IDS: readonly string[];
4
+ export declare const CURATED_METHOD_TOOLS: Record<string, string[]>;
5
+ export declare const CUD_OVERRIDES: Record<string, Cud>;
@@ -1,4 +1,3 @@
1
- // GENERATED by scripts/gen-tools.ts — do not edit. Regenerate: npm run gen:tools
2
1
  export const GENERATED_METHOD_TOOLS = {
3
2
  "admin.channels.stop": "admin_channels_stop",
4
3
  "admin.customer.devices.chromeos.batchChangeStatus": "admin_customer_devices_chromeos_batch_change_status",
@@ -892,3 +891,159 @@ export const CURATED_METHOD_IDS = [
892
891
  "webmasters.sites.get",
893
892
  "webmasters.sites.list",
894
893
  ];
894
+ // Every curated tool name registered on each curated method id (see
895
+ // gen-config.ts: derived from the curated registrations themselves).
896
+ export const CURATED_METHOD_TOOLS = {
897
+ "analyticsadmin.accountSummaries.list": ["analytics_account_summaries"],
898
+ "analyticsdata.properties.getMetadata": ["analytics_get_metadata"],
899
+ "analyticsdata.properties.runRealtimeReport": ["analytics_run_realtime_report"],
900
+ "analyticsdata.properties.runReport": ["analytics_run_report"],
901
+ "calendar.calendarList.list": ["calendar_list_calendars"],
902
+ "calendar.calendars.insert": ["calendar_create_calendar"],
903
+ "calendar.events.delete": ["calendar_delete_event"],
904
+ "calendar.events.get": ["calendar_get_event", "calendar_respond_event", "calendar_update_event"],
905
+ "calendar.events.insert": ["calendar_create_event"],
906
+ "calendar.events.instances": ["calendar_list_instances"],
907
+ "calendar.events.list": ["calendar_list_events"],
908
+ "calendar.events.move": ["calendar_move_event"],
909
+ "calendar.events.patch": ["calendar_respond_event", "calendar_update_event"],
910
+ "calendar.events.quickAdd": ["calendar_quick_add"],
911
+ "calendar.freebusy.query": ["calendar_get_freebusy"],
912
+ "chat.spaces.get": ["chat_spaces_get"],
913
+ "chat.spaces.list": ["chat_spaces_list"],
914
+ "chat.spaces.messages.create": ["chat_messages_create"],
915
+ "chat.spaces.messages.list": ["chat_messages_list"],
916
+ "directory.groups.list": ["admin_groups_list"],
917
+ "directory.members.list": ["admin_group_members_list"],
918
+ "directory.users.get": ["admin_users_get"],
919
+ "directory.users.list": ["admin_users_list"],
920
+ "directory.users.patch": ["admin_users_update"],
921
+ "docs.documents.batchUpdate": ["docs_add_tab", "docs_batch_update", "docs_create_footer", "docs_create_header", "docs_create_named_range", "docs_create_paragraph_bullets", "docs_delete_footer", "docs_delete_header", "docs_delete_named_range", "docs_delete_paragraph_bullets", "docs_delete_range", "docs_delete_tab", "docs_insert_inline_image", "docs_insert_page_break", "docs_insert_section_break", "docs_insert_table", "docs_insert_text", "docs_modify_table", "docs_replace_named_range_content", "docs_replace_text", "docs_update_document_style", "docs_update_paragraph_style", "docs_update_style", "docs_update_tab_properties"],
922
+ "docs.documents.create": ["docs_create"],
923
+ "docs.documents.get": ["docs_get", "docs_read"],
924
+ "drive.about.get": ["drive_get_about"],
925
+ "drive.accessproposals.list": ["drive_access_proposal_list"],
926
+ "drive.accessproposals.resolve": ["drive_access_proposal_resolve"],
927
+ "drive.comments.create": ["drive_comment_create"],
928
+ "drive.comments.delete": ["drive_comment_delete"],
929
+ "drive.comments.get": ["drive_comment_get"],
930
+ "drive.comments.list": ["drive_comment_list"],
931
+ "drive.comments.update": ["drive_comment_update"],
932
+ "drive.drives.get": ["drive_shared_drive_get"],
933
+ "drive.drives.list": ["drive_shared_drives_list"],
934
+ "drive.files.copy": ["drive_copy", "drive_transfer"],
935
+ "drive.files.create": ["drive_create_folder", "drive_transfer", "drive_upload"],
936
+ "drive.files.delete": ["drive_delete"],
937
+ "drive.files.emptyTrash": ["drive_empty_trash"],
938
+ "drive.files.export": ["drive_export", "drive_read", "drive_transfer"],
939
+ "drive.files.get": ["drive_download", "drive_export", "drive_move", "drive_read", "drive_transfer", "drive_update"],
940
+ "drive.files.list": ["drive_list", "drive_search"],
941
+ "drive.files.update": ["drive_move", "drive_transfer", "drive_trash", "drive_untrash", "drive_update"],
942
+ "drive.permissions.create": ["drive_share", "drive_transfer"],
943
+ "drive.permissions.delete": ["drive_remove_permission", "drive_transfer"],
944
+ "drive.permissions.list": ["drive_list_permissions"],
945
+ "drive.permissions.update": ["drive_permission_update"],
946
+ "drive.replies.create": ["drive_reply_create"],
947
+ "drive.replies.delete": ["drive_reply_delete"],
948
+ "drive.replies.list": ["drive_reply_list"],
949
+ "drive.replies.update": ["drive_reply_update"],
950
+ "drive.revisions.delete": ["drive_revision_delete"],
951
+ "drive.revisions.list": ["drive_revision_list"],
952
+ "drive.revisions.update": ["drive_revision_update"],
953
+ "forms.forms.batchUpdate": ["forms_batch_update"],
954
+ "forms.forms.create": ["forms_create"],
955
+ "forms.forms.get": ["forms_get"],
956
+ "forms.forms.responses.get": ["forms_response_get"],
957
+ "forms.forms.responses.list": ["forms_responses_list"],
958
+ "forms.forms.setPublishSettings": ["forms_set_publish_settings"],
959
+ "forms.forms.watches.list": ["forms_watches_list"],
960
+ "gmail.users.drafts.create": ["gmail_create_draft"],
961
+ "gmail.users.drafts.delete": ["gmail_delete_draft"],
962
+ "gmail.users.drafts.get": ["gmail_get_draft", "gmail_send_draft"],
963
+ "gmail.users.drafts.list": ["gmail_list_drafts"],
964
+ "gmail.users.drafts.send": ["gmail_send_draft"],
965
+ "gmail.users.getProfile": ["gmail_get_profile"],
966
+ "gmail.users.history.list": ["gmail_list_history"],
967
+ "gmail.users.labels.create": ["gmail_create_label"],
968
+ "gmail.users.labels.delete": ["gmail_delete_label"],
969
+ "gmail.users.labels.list": ["gmail_list_labels"],
970
+ "gmail.users.messages.attachments.get": ["gmail_download_attachment"],
971
+ "gmail.users.messages.batchDelete": ["gmail_batch_delete"],
972
+ "gmail.users.messages.batchModify": ["gmail_batch_modify"],
973
+ "gmail.users.messages.delete": ["gmail_delete"],
974
+ "gmail.users.messages.get": ["gmail_create_draft", "gmail_read", "gmail_read_batch", "gmail_search", "gmail_send"],
975
+ "gmail.users.messages.list": ["gmail_search"],
976
+ "gmail.users.messages.modify": ["gmail_modify_labels"],
977
+ "gmail.users.messages.send": ["gmail_send"],
978
+ "gmail.users.messages.trash": ["gmail_trash"],
979
+ "gmail.users.settings.getVacation": ["gmail_get_vacation"],
980
+ "gmail.users.settings.updateVacation": ["gmail_set_vacation"],
981
+ "gmail.users.threads.get": ["gmail_read_thread"],
982
+ "meet.conferenceRecords.get": ["meet_conference_record_get"],
983
+ "meet.conferenceRecords.list": ["meet_conference_records_list"],
984
+ "meet.conferenceRecords.recordings.list": ["meet_recordings_list"],
985
+ "meet.conferenceRecords.transcripts.entries.list": ["meet_transcript_entries_list"],
986
+ "meet.conferenceRecords.transcripts.list": ["meet_transcripts_list"],
987
+ "people.contactGroups.create": ["contacts_group_create"],
988
+ "people.contactGroups.get": ["contacts_group_members"],
989
+ "people.contactGroups.list": ["contacts_groups_list"],
990
+ "people.people.connections.list": ["contacts_list"],
991
+ "people.people.createContact": ["contacts_create"],
992
+ "people.people.deleteContact": ["contacts_delete"],
993
+ "people.people.get": ["contacts_get", "contacts_update"],
994
+ "people.people.getBatchGet": ["contacts_group_members"],
995
+ "people.people.searchContacts": ["contacts_resolve", "contacts_search"],
996
+ "people.people.updateContact": ["contacts_update"],
997
+ "reports.activities.list": ["reports_activities_list"],
998
+ "searchconsole.urlInspection.index.inspect": ["searchconsole_url_inspect"],
999
+ "sheets.spreadsheets.batchUpdate": ["sheets_add_conditional_format_rule", "sheets_add_named_range", "sheets_add_sheet", "sheets_auto_resize_dimensions", "sheets_batch_update", "sheets_clear_basic_filter", "sheets_delete_dimension", "sheets_delete_named_range", "sheets_delete_sheet", "sheets_duplicate_sheet", "sheets_find_replace", "sheets_format_cells", "sheets_insert_dimension", "sheets_merge_cells", "sheets_set_basic_filter", "sheets_set_data_validation", "sheets_sort_range", "sheets_unmerge_cells", "sheets_update_borders", "sheets_update_sheet_properties"],
1000
+ "sheets.spreadsheets.create": ["sheets_create"],
1001
+ "sheets.spreadsheets.get": ["sheets_format_cells", "sheets_get"],
1002
+ "sheets.spreadsheets.values.append": ["sheets_append_rows"],
1003
+ "sheets.spreadsheets.values.batchClear": ["sheets_batch_clear"],
1004
+ "sheets.spreadsheets.values.batchGet": ["sheets_batch_read"],
1005
+ "sheets.spreadsheets.values.batchUpdate": ["sheets_batch_write"],
1006
+ "sheets.spreadsheets.values.clear": ["sheets_clear_range"],
1007
+ "sheets.spreadsheets.values.get": ["sheets_read_range"],
1008
+ "sheets.spreadsheets.values.update": ["sheets_write_range"],
1009
+ "slides.presentations.batchUpdate": ["slides_batch_update"],
1010
+ "slides.presentations.create": ["slides_create"],
1011
+ "slides.presentations.get": ["slides_get"],
1012
+ "slides.presentations.pages.get": ["slides_page_get"],
1013
+ "slides.presentations.pages.getThumbnail": ["slides_page_thumbnail"],
1014
+ "tasks.tasklists.delete": ["tasks_list_delete"],
1015
+ "tasks.tasklists.get": ["tasks_list_get"],
1016
+ "tasks.tasklists.insert": ["tasks_list_insert"],
1017
+ "tasks.tasklists.list": ["tasks_lists_list"],
1018
+ "tasks.tasklists.patch": ["tasks_list_update"],
1019
+ "tasks.tasks.clear": ["tasks_clear"],
1020
+ "tasks.tasks.delete": ["tasks_delete"],
1021
+ "tasks.tasks.get": ["tasks_get"],
1022
+ "tasks.tasks.insert": ["tasks_insert"],
1023
+ "tasks.tasks.list": ["tasks_list"],
1024
+ "tasks.tasks.move": ["tasks_move"],
1025
+ "tasks.tasks.patch": ["tasks_update"],
1026
+ "webmasters.searchanalytics.query": ["searchconsole_searchanalytics_query"],
1027
+ "webmasters.sitemaps.delete": ["searchconsole_sitemaps_delete"],
1028
+ "webmasters.sitemaps.get": ["searchconsole_sitemaps_get"],
1029
+ "webmasters.sitemaps.list": ["searchconsole_sitemaps_list"],
1030
+ "webmasters.sitemaps.submit": ["searchconsole_sitemaps_submit"],
1031
+ "webmasters.sites.add": ["searchconsole_sites_add"],
1032
+ "webmasters.sites.delete": ["searchconsole_sites_delete"],
1033
+ "webmasters.sites.get": ["searchconsole_sites_get"],
1034
+ "webmasters.sites.list": ["searchconsole_sites_list"],
1035
+ };
1036
+ // A runtime copy of scripts/gen-config.ts's own CUD_OVERRIDES (scripts/ and
1037
+ // src/ are separate compilation roots, so src/ code cannot import the build
1038
+ // script directly). google-api.ts applies this the same way gen-tools.ts
1039
+ // does when generating a method's twin (CUD_OVERRIDES[id] ?? cudFromMethod),
1040
+ // so the escape hatch and a method's generated twin always agree on its
1041
+ // write class, including for the methods HTTP-verb inference misreads.
1042
+ export const CUD_OVERRIDES = {
1043
+ "admin.customer.devices.chromeos.batchChangeStatus": "delete",
1044
+ "admin.customer.devices.chromeos.issueCommand": "delete",
1045
+ "analyticsadmin.properties.acknowledgeUserDataCollection": "update",
1046
+ "directory.chromeosdevices.action": "delete",
1047
+ "directory.mobiledevices.action": "delete",
1048
+ "searchconsole.urlTestingTools.mobileFriendlyTest.run": "read",
1049
+ };
@@ -18,6 +18,12 @@ interface ReplyDerivation {
18
18
  * to/cc/subject. On any fetch failure, degrade to the API id for threading and
19
19
  * report sourceFound:false so the caller can decide. */
20
20
  export declare function resolveReply(gmail: any, account: string, primaryEmail: string, replyToMessageId: string, replyAll: boolean): Promise<ReplyDerivation>;
21
+ /** Reads attachment files into buffers (THIS server reads them, never
22
+ * MailComposer), enforces absolute-path + total-size guards, and derives the
23
+ * MIME filename via basename so a caller name can't inject path separators.
24
+ * The absolute-path + config/token-dir confinement rule itself lives in
25
+ * assertLocalPathAllowed (src/tools/_local-files.ts), shared with Drive's
26
+ * upload/update/download/export so the two surfaces cannot drift apart. */
21
27
  export declare function readAttachments(raw: Array<{
22
28
  path: string;
23
29
  filename?: string;