mcp-google-multi 6.0.0-beta.3 → 6.0.0-beta.4

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.
Files changed (91) hide show
  1. package/README.md +5 -5
  2. package/dist/accounts.d.ts +3 -3
  3. package/dist/accounts.js +18 -31
  4. package/dist/arg-normalize.js +15 -35
  5. package/dist/arg-strict.js +8 -28
  6. package/dist/auth.d.ts +1 -1
  7. package/dist/auth.js +5 -10
  8. package/dist/boot-gates.d.ts +1 -1
  9. package/dist/boot-gates.js +3 -5
  10. package/dist/cimd-cache.js +5 -11
  11. package/dist/client-config.d.ts +2 -2
  12. package/dist/client-config.js +8 -14
  13. package/dist/client.js +0 -0
  14. package/dist/compose.d.ts +8 -1
  15. package/dist/compose.js +11 -11
  16. package/dist/config-file.js +7 -12
  17. package/dist/discover.js +4 -9
  18. package/dist/discovery-client.js +14 -33
  19. package/dist/doctor.d.ts +3 -3
  20. package/dist/doctor.js +12 -18
  21. package/dist/env-load.js +2 -5
  22. package/dist/executor.js +2 -4
  23. package/dist/fanout.js +8 -17
  24. package/dist/fs-atomic.d.ts +0 -3
  25. package/dist/fs-atomic.js +6 -10
  26. package/dist/hint-provider.js +4 -10
  27. package/dist/http-config.d.ts +2 -2
  28. package/dist/http-config.js +3 -7
  29. package/dist/http-transport.d.ts +5 -5
  30. package/dist/http-transport.js +39 -92
  31. package/dist/identity.d.ts +2 -2
  32. package/dist/identity.js +6 -9
  33. package/dist/index.js +19 -31
  34. package/dist/master-key.d.ts +3 -3
  35. package/dist/master-key.js +11 -22
  36. package/dist/mcp-token.d.ts +3 -13
  37. package/dist/mcp-token.js +16 -32
  38. package/dist/metrics-cli.d.ts +1 -1
  39. package/dist/metrics-cli.js +1 -1
  40. package/dist/metrics-tap.js +2 -5
  41. package/dist/migrate-config.d.ts +1 -2
  42. package/dist/migrate-config.js +3 -5
  43. package/dist/net-tuning.js +2 -4
  44. package/dist/oauth-as.d.ts +2 -2
  45. package/dist/oauth-as.js +32 -73
  46. package/dist/oauth-consent.js +4 -10
  47. package/dist/outbound-allowlist.js +4 -9
  48. package/dist/reauth-hint.js +2 -5
  49. package/dist/registry-transfer.d.ts +1 -1
  50. package/dist/registry-transfer.js +5 -13
  51. package/dist/registry.d.ts +1 -9
  52. package/dist/registry.js +38 -66
  53. package/dist/scope-catalog.d.ts +5 -0
  54. package/dist/scope-catalog.js +17 -7
  55. package/dist/scope-observability.d.ts +2 -3
  56. package/dist/scope-observability.js +8 -14
  57. package/dist/services.d.ts +3 -0
  58. package/dist/services.js +5 -10
  59. package/dist/ssrf-guard.js +11 -22
  60. package/dist/tenant-purge.js +2 -4
  61. package/dist/token-store.js +0 -0
  62. package/dist/tools/_coerce.d.ts +4 -4
  63. package/dist/tools/_errors.js +22 -39
  64. package/dist/tools/_local-files.js +4 -8
  65. package/dist/tools/account-wizard.d.ts +4 -3
  66. package/dist/tools/account-wizard.js +48 -54
  67. package/dist/tools/accounts-tool.d.ts +1 -1
  68. package/dist/tools/accounts-tool.js +1 -1
  69. package/dist/tools/admin.js +2 -3
  70. package/dist/tools/analytics.js +4 -5
  71. package/dist/tools/calendar.js +4 -7
  72. package/dist/tools/chat.js +2 -3
  73. package/dist/tools/contacts.js +8 -9
  74. package/dist/tools/docs.js +4 -9
  75. package/dist/tools/drive.js +18 -27
  76. package/dist/tools/forms.js +2 -3
  77. package/dist/tools/gmail-mime.d.ts +1 -1
  78. package/dist/tools/gmail-mime.js +7 -12
  79. package/dist/tools/gmail.d.ts +2 -2
  80. package/dist/tools/gmail.js +26 -40
  81. package/dist/tools/google-api.js +5 -7
  82. package/dist/tools/meet.js +2 -3
  83. package/dist/tools/searchconsole.js +2 -3
  84. package/dist/tools/sheets.js +2 -3
  85. package/dist/tools/slides.js +2 -3
  86. package/dist/tools/tasks.js +2 -3
  87. package/dist/trim.js +2 -3
  88. package/dist/usage-metrics.js +5 -12
  89. package/dist/write-control.d.ts +4 -8
  90. package/dist/write-control.js +3 -4
  91. package/package.json +21 -21
@@ -24,9 +24,8 @@ const GOOGLE_WORKSPACE_TYPES = new Set([
24
24
  'application/vnd.google-apps.presentation',
25
25
  'application/vnd.google-apps.drawing',
26
26
  ]);
27
- // drive_read inlines only textual content. Beyond text/*, RFC 6839 structured-
28
- // syntax suffixes (+json/+xml/...) and a few bare application/* types are text
29
- // in practice — image/svg+xml was the motivating false "binary" refusal.
27
+ // drive_read inlines only textual content; RFC 6839 +json/+xml/... suffixes and
28
+ // a few bare application/* types count too (image/svg+xml was the motivating case).
30
29
  const TEXTUAL_EXACT = new Set([
31
30
  'application/json',
32
31
  'application/xml',
@@ -81,17 +80,14 @@ export const DRIVE_QUERY_HINT = "Drive search syntax: a plain keyword is treated
81
80
  "structured query needs an operator, e.g. \"name contains 'report'\", " +
82
81
  "\"mimeType = 'application/pdf'\", or \"'me' in owners\". " +
83
82
  'Reference: https://developers.google.com/drive/api/guides/search-files';
84
- // A bare keyword is not valid Drive `q` syntax (Google 400s "Invalid Value"), yet
85
- // it is the natural thing to pass to a "search" tool, so wrap it as a full-text
86
- // match. A query already carrying a comparison operator or a quoted literal is a
87
- // structured query and is passed through untouched.
83
+ // A bare keyword isn't valid Drive `q` syntax (400s "Invalid Value"), so wrap it
84
+ // as a full-text match; a query with a comparison operator is passed through untouched.
88
85
  export function normalizeDriveQuery(raw) {
89
86
  const q = raw.trim();
90
87
  if (q === '')
91
88
  return q;
92
- // Structured iff it carries a comparison operator or the membership form
93
- // "<value> in (parents|owners|writers|readers)". A bare apostrophe does NOT
94
- // count (names like O'Brien are keywords, not query syntax).
89
+ // Structured iff it has a comparison operator or the membership form "<value> in
90
+ // (parents|owners|writers|readers)"; a bare apostrophe (e.g. O'Brien) doesn't count.
95
91
  const hasComparison = /(\bcontains\b|!=|<=|>=|[=<>])/i.test(q);
96
92
  const hasMembership = /\bin\s+(parents|owners|writers|readers)\b/i.test(q);
97
93
  if (hasComparison || hasMembership)
@@ -99,9 +95,8 @@ export function normalizeDriveQuery(raw) {
99
95
  const escaped = q.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
100
96
  return `fullText contains '${escaped}'`;
101
97
  }
102
- // Drive rejects a malformed query with HTTP 400 / reason "invalid" and the opaque
103
- // message "Invalid Value"; detect that so drive_search returns query-syntax help
104
- // instead of a generic upstream_error.
98
+ // Drive 400s a malformed query with the opaque reason "invalid"/"Invalid Value";
99
+ // detect it so drive_search returns query-syntax help instead of a generic upstream_error.
105
100
  export function isDriveInvalidQuery(error) {
106
101
  const raw = error?.code ?? error?.status ?? error?.response?.status;
107
102
  const status = typeof raw === 'string' ? Number(raw) : raw;
@@ -122,9 +117,8 @@ export function resolveShareNotification(opts) {
122
117
  return opts.sendNotification ?? true;
123
118
  }
124
119
  export function registerDriveTools(server, deps = {}) {
125
- // Per-registry, LIVE account enum + injectable client (S1.10): the
126
- // schema follows the registry's account view at parse time, and the
127
- // custody path is the context's, not the process global.
120
+ // Per-registry, live account enum + injectable client: the schema follows
121
+ // the registry's account view at parse time, not a process-global default.
128
122
  const accountEnum = accountArgLive(() => server.accountAliases()).optional();
129
123
  const requiredAccountEnum = accountArgLive(() => server.accountAliases());
130
124
  const getClientFn = deps.getClientFn ?? getClient;
@@ -144,8 +138,8 @@ export function registerDriveTools(server, deps = {}) {
144
138
  },
145
139
  }, async ({ account, query, maxResults, driveId, pageToken }) => {
146
140
  try {
147
- // normalizeDriveQuery trims to '' and Drive reads that as "everything",
148
- // so a blank query returned a raw directory listing as a search result.
141
+ // normalizeDriveQuery trims to '' and Drive reads that as "everything", so
142
+ // a blank query would list the whole drive as a "search" result.
149
143
  if (query.trim() === '') {
150
144
  return invalidParams(account, '`query` is empty, and Drive reads an empty query as "match everything".', 'Pass a search term, or a structured Drive query such as "mimeType = \'application/pdf\'". To browse a folder instead, use drive_list.');
151
145
  }
@@ -154,9 +148,8 @@ export function registerDriveTools(server, deps = {}) {
154
148
  const params = {
155
149
  q: normalizeDriveQuery(query),
156
150
  pageSize: maxResults ?? 10,
157
- // nextPageToken and incompleteSearch live OUTSIDE files(...): omit
158
- // them from the mask and Drive drops them, so a partial result was
159
- // indistinguishable from a complete one.
151
+ // nextPageToken and incompleteSearch live OUTSIDE files(...): omit them from
152
+ // the mask and Drive drops them, making a partial result indistinguishable from a complete one.
160
153
  fields: 'nextPageToken,incompleteSearch,files(id,name,mimeType,modifiedTime,webViewLink,size,parents,driveId)',
161
154
  supportsAllDrives: true,
162
155
  includeItemsFromAllDrives: true,
@@ -182,9 +175,8 @@ export function registerDriveTools(server, deps = {}) {
182
175
  }
183
176
  catch (error) {
184
177
  if (isDriveInvalidQuery(error)) {
185
- // Through safeMessage and stringifyEnvelope like every other
186
- // envelope: this path used to read the upstream body directly, so a
187
- // non-JSON response landed here uncapped.
178
+ // Goes through safeMessage/stringifyEnvelope like every other envelope
179
+ // so a non-JSON upstream response can't land here uncapped.
188
180
  return {
189
181
  content: [{ type: 'text', text: stringifyEnvelope({
190
182
  error: 'invalid_query',
@@ -685,9 +677,8 @@ export function registerDriveTools(server, deps = {}) {
685
677
  },
686
678
  }, async ({ account, fileId, type, role, emailAddress, domain, sendNotification, emailMessage, transferOwnership, expirationTime }) => {
687
679
  {
688
- // Outbound allowlist (off unless GOOGLE_OUTBOUND_ALLOWLIST is set):
689
- // user/group grantees must match; a domain share needs its exact
690
- // "@domain" entry; "anyone" (link sharing) is refused while active.
680
+ // Outbound allowlist (off unless GOOGLE_OUTBOUND_ALLOWLIST is set): user/group
681
+ // grantees must match, a domain share needs its exact "@domain" entry, "anyone" is refused while active.
691
682
  const list = resolveOutboundAllowlist();
692
683
  if (list) {
693
684
  if ((type === 'user' || type === 'group') && emailAddress) {
@@ -5,9 +5,8 @@ import { getClient } from '../client.js';
5
5
  import { handleGoogleApiError } from './_errors.js';
6
6
  import { coerceArray, coerceBoolean, coerceJson } from './_coerce.js';
7
7
  export function registerFormsTools(server, deps = {}) {
8
- // Per-registry, LIVE account enum + injectable client (S1.10): the
9
- // schema follows the registry's account view at parse time, and the
10
- // custody path is the context's, not the process global.
8
+ // Per-registry, live account enum + injectable client: the schema follows
9
+ // the registry's account view at parse time, not a process-global default.
11
10
  const accountEnum = accountArgLive(() => server.accountAliases()).optional();
12
11
  const getClientFn = deps.getClientFn ?? getClient;
13
12
  server.registerTool('forms_get', {
@@ -37,7 +37,7 @@ export declare class HeaderInjectionError extends Error {
37
37
  constructor(field: string);
38
38
  }
39
39
  /**
40
- * The single MIME-assembly seam for gmail_send and gmail_create_draft (A4).
40
+ * The single MIME-assembly seam for gmail_send and gmail_create_draft.
41
41
  * MailComposer owns header/RFC-2047/address encoding and boundary generation,
42
42
  * which closes the CRLF header-injection hole in the old hand-rolled encoders.
43
43
  * A deterministic pre-check rejects CR/LF in address/subject headers with a
@@ -3,10 +3,8 @@ import MarkdownIt from 'markdown-it';
3
3
  import TurndownService from 'turndown';
4
4
  // @ts-expect-error turndown-plugin-gfm ships no type declarations
5
5
  import { gfm } from 'turndown-plugin-gfm';
6
- // D6 send: one symmetric text format. body is Markdown; text/plain = the
7
- // source verbatim, text/html = this render. html:false ESCAPES raw HTML in
8
- // the body (the XSS-safe default); allowRawHtml swaps to the html:true
9
- // instance so an audited caller can pass literal HTML (e.g. inline color).
6
+ // Send: body is Markdown; text/plain is the source, text/html is this render.
7
+ // html:false escapes raw HTML in the body (XSS-safe default); allowRawHtml opts into html:true for an audited caller.
10
8
  const mdSafe = new MarkdownIt({ html: false, linkify: true });
11
9
  const mdRaw = new MarkdownIt({ html: true, linkify: true });
12
10
  export function renderMarkdown(body, allowRawHtml = false) {
@@ -87,10 +85,8 @@ export function htmlToText(html) {
87
85
  .replace(/\n{3,}/g, '\n\n')
88
86
  .trim();
89
87
  }
90
- // D6 read: HTML-only bodies convert to Markdown so the model reads structure
91
- // (headings, links, lists, GFM tables), not a flat text dump. turndown's
92
- // bundled @mixmark-io/domino fork does NOT execute scripts; remove() drops
93
- // script/style/head entirely (turndown otherwise leaks their text content).
88
+ // HTML-only bodies convert to Markdown so the model reads structure, not a flat
89
+ // text dump; turndown's bundled domino fork doesn't execute scripts, and remove() drops script/style/head (which otherwise leak text).
94
90
  const turndownService = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced', bulletListMarker: '-' });
95
91
  turndownService.use(gfm);
96
92
  turndownService.remove(['script', 'style', 'head']);
@@ -125,7 +121,7 @@ export class HeaderInjectionError extends Error {
125
121
  }
126
122
  }
127
123
  /**
128
- * The single MIME-assembly seam for gmail_send and gmail_create_draft (A4).
124
+ * The single MIME-assembly seam for gmail_send and gmail_create_draft.
129
125
  * MailComposer owns header/RFC-2047/address encoding and boundary generation,
130
126
  * which closes the CRLF header-injection hole in the old hand-rolled encoders.
131
127
  * A deterministic pre-check rejects CR/LF in address/subject headers with a
@@ -152,9 +148,8 @@ export async function composeRaw(input) {
152
148
  attachments: input.attachments,
153
149
  newline: '\r\n',
154
150
  textEncoding: 'base64',
155
- // This server reads any attachment file itself and passes a Buffer;
156
- // MailComposer must never touch the filesystem or network (SSRF / local
157
- // file-read defense-in-depth).
151
+ // This server reads attachment files itself and passes a Buffer; MailComposer
152
+ // must never touch the filesystem or network (SSRF / local file-read defense-in-depth).
158
153
  disableFileAccess: true,
159
154
  disableUrlAccess: true,
160
155
  };
@@ -16,7 +16,7 @@ interface ReplyDerivation {
16
16
  }
17
17
  /** Fetch the reply source once and derive threading headers plus (auto-fill)
18
18
  * to/cc/subject. On any fetch failure, degrade to the API id for threading and
19
- * report sourceFound:false so the caller can decide (per A8 error dispositions). */
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
21
  export declare function readAttachments(raw: Array<{
22
22
  path: string;
@@ -27,7 +27,7 @@ export declare function parseMessage(msg: any, bodyCap?: number, opts?: {
27
27
  rawHtml?: boolean;
28
28
  }): GmailMessageFull;
29
29
  /**
30
- * A9 gmail_read_batch core: fetch N ids with bounded parallelism, parse each,
30
+ * gmail_read_batch core: fetch N ids with bounded parallelism, parse each,
31
31
  * and bound the aggregate output. Order is preserved by input index. A per-id
32
32
  * failure becomes a `{ id, error }` entry; an account-wide auth/scope failure
33
33
  * is rethrown so the caller maps it to a whole-call isError. Returns the
@@ -38,16 +38,15 @@ function decodeBody(payload, rawHtml = false) {
38
38
  const plain = [];
39
39
  const html = [];
40
40
  collectTextParts(payload, plain, html, true);
41
- // Concatenating every plain leaf keeps forwarded/mixed messages whole;
42
- // any plain content beats HTML because alternatives duplicate the same body.
43
- // A plain leaf is returned verbatim (byte-identical to v5).
41
+ // Concatenating every plain leaf keeps forwarded/mixed messages whole; plain
42
+ // always wins over HTML since alternatives duplicate the same body.
44
43
  if (plain.length > 0)
45
44
  return { body: plain.join('\n\n'), bodyFormat: 'plain' };
46
45
  if (html.length > 0) {
47
46
  const joined = html.join('\n\n');
48
47
  if (rawHtml)
49
48
  return { body: joined, bodyFormat: 'html' };
50
- // HTML-only -> Markdown (D6). turndown failure degrades to plain text.
49
+ // HTML-only -> Markdown. turndown failure degrades to plain text.
51
50
  const { text, ok } = htmlToMarkdown(joined);
52
51
  return { body: text, bodyFormat: ok ? 'markdown' : 'plain' };
53
52
  }
@@ -91,9 +90,8 @@ function parseAddresses(headerValue) {
91
90
  return out;
92
91
  }
93
92
  const normAddr = (a) => a.trim().toLowerCase();
94
- /** Rebuild a comma-separated address header; composeRaw/MailComposer re-encodes
95
- * names (RFC 2047) and the A4 CRLF pre-check applies, so derived recipients get
96
- * the same header-injection guard as caller-supplied ones. */
93
+ /** Rebuilds a comma-separated address header; composeRaw/MailComposer re-encodes
94
+ * names and apply the same header-injection guard as caller-supplied recipients. */
97
95
  function formatAddresses(list) {
98
96
  return list.map((a) => (a.name ? `${a.name} <${a.address}>` : a.address)).join(', ');
99
97
  }
@@ -103,10 +101,8 @@ export function deriveReplySubject(sourceSubject) {
103
101
  const s = sourceSubject.trim();
104
102
  return /^re:/i.test(s) ? s : `Re: ${s}`;
105
103
  }
106
- // The account's own-address set (primary + Gmail send-as aliases) used to
107
- // exclude the caller from reply-all. Send-as rarely changes, so memoize per
108
- // process; a stale miss only costs one extra self-copy, never a wrong send.
109
- // Keyed by alias AND mailbox: two contexts can share an alias name.
104
+ // Own-address set (primary + send-as aliases) for excluding the caller from
105
+ // reply-all; memoized per-process (a stale miss just costs one self-copy), keyed by alias+mailbox.
110
106
  const ownAddressCache = new Map();
111
107
  function getOwnAddresses(gmail, account, primaryEmail) {
112
108
  const key = `${account} ${normAddr(primaryEmail)}`;
@@ -122,7 +118,7 @@ function getOwnAddresses(gmail, account, primaryEmail) {
122
118
  }
123
119
  }
124
120
  catch {
125
- // insufficient_scope / 5xx: degrade to the primary alone, never block the send.
121
+ // insufficient_scope/5xx: degrade silently
126
122
  }
127
123
  return set;
128
124
  })();
@@ -132,7 +128,7 @@ function getOwnAddresses(gmail, account, primaryEmail) {
132
128
  }
133
129
  /** Fetch the reply source once and derive threading headers plus (auto-fill)
134
130
  * to/cc/subject. On any fetch failure, degrade to the API id for threading and
135
- * report sourceFound:false so the caller can decide (per A8 error dispositions). */
131
+ * report sourceFound:false so the caller can decide. */
136
132
  export async function resolveReply(gmail, account, primaryEmail, replyToMessageId, replyAll) {
137
133
  let headers;
138
134
  try {
@@ -145,7 +141,7 @@ export async function resolveReply(gmail, account, primaryEmail, replyToMessageI
145
141
  headers = meta.data.payload?.headers;
146
142
  }
147
143
  catch {
148
- // Degrade to the API id (pre-lookup behavior) rather than blocking the send.
144
+ // Degrade to the API id rather than blocking the send.
149
145
  return { inReplyTo: replyToMessageId, references: replyToMessageId, sourceFound: false };
150
146
  }
151
147
  const threading = buildReplyHeaders(replyToMessageId, getHeader(headers, 'Message-ID'), getHeader(headers, 'References'));
@@ -181,15 +177,11 @@ export async function resolveReply(gmail, account, primaryEmail, replyToMessageI
181
177
  };
182
178
  }
183
179
  const BODY_CAP_CHARS = 50_000;
184
- // gmail_read_batch aggregate ceiling: a batch of up to 100 messages must never
185
- // blow the context window, so the summed serialized output is bounded here
186
- // (matches the tool's anthropic/maxResultSizeChars hint). Per-message bodies are
187
- // still individually capped at BODY_CAP_CHARS first.
180
+ // gmail_read_batch aggregate ceiling: bounds the summed serialized output so a
181
+ // 100-message batch can't blow the context window (per-message bodies are capped at BODY_CAP_CHARS first).
188
182
  const BATCH_MAX_RESULT_CHARS = 100_000;
189
- // Gmail's messages.send raw/JSON path rejects messages near ~25 MB (the wire
190
- // message is base64-encoded, ~+33% over the raw bytes), so cap the ESTIMATED
191
- // ENCODED total there rather than the spec's nominal 35 MB raw (spike/live
192
- // correction — a 35 MB raw payload is ~47 MB on the wire and Gmail 400s it).
183
+ // messages.send rejects near ~25MB on the wire (base64 adds ~33%), so cap the
184
+ // estimated ENCODED total here rather than the nominal 35MB raw limit, which 400s at ~47MB wire.
193
185
  const GMAIL_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
194
186
  const estimateEncoded = (rawBytes) => Math.ceil((rawBytes * 4) / 3);
195
187
  const attachmentSchema = z
@@ -203,10 +195,8 @@ const attachmentSchema = z
203
195
  /** Reads attachment files into buffers (THIS server reads them, never
204
196
  * MailComposer), enforces absolute-path + total-size guards, and derives the
205
197
  * MIME filename via basename so a caller name can't inject path separators. */
206
- // Deny reading anything inside the server's own secret dirs — mailing out
207
- // master.key / <alias>.enc / mcp-jwt.key would be a full-account-takeover
208
- // exfil channel (this same server also reads untrusted mail/drive content, so
209
- // a prompt-injected attach path is a real confused-deputy vector).
198
+ // Deny reading inside the server's own secret dirs; mailing out master.key /
199
+ // <alias>.enc / mcp-jwt.key would be account takeover (also guards against a prompt-injected attach path).
210
200
  function isInside(dir, target) {
211
201
  let d = path.resolve(dir);
212
202
  let t = path.resolve(target);
@@ -280,9 +270,8 @@ export async function readAttachments(raw, bodyBytes) {
280
270
  }
281
271
  return out;
282
272
  }
283
- /** Tagged compose-time failure; the handlers map it to an isError envelope.
284
- * `category` is the taxonomy error field (default validation_error); reply
285
- * auto-fill raises it as not_found when a source message can't be fetched. */
273
+ /** Tagged compose-time failure mapped to an isError envelope; `category`
274
+ * defaults to validation_error (reply auto-fill raises not_found instead). */
286
275
  class GmailComposeError extends Error {
287
276
  slug;
288
277
  category;
@@ -336,7 +325,7 @@ export function parseMessage(msg, bodyCap, opts) {
336
325
  };
337
326
  }
338
327
  /**
339
- * A9 gmail_read_batch core: fetch N ids with bounded parallelism, parse each,
328
+ * gmail_read_batch core: fetch N ids with bounded parallelism, parse each,
340
329
  * and bound the aggregate output. Order is preserved by input index. A per-id
341
330
  * failure becomes a `{ id, error }` entry; an account-wide auth/scope failure
342
331
  * is rethrown so the caller maps it to a whole-call isError. Returns the
@@ -383,9 +372,8 @@ export async function readBatch(gmail, account, ids, full, rawHtml) {
383
372
  }
384
373
  }
385
374
  }
386
- // Aggregate guard: walk in order, accumulating serialized size. Once the
387
- // running total would exceed the maxResultSizeChars budget, cap this and every
388
- // later success body so a big batch never blows the context window.
375
+ // Aggregate guard: walk in order accumulating serialized size; once the running
376
+ // total would exceed the budget, cap this and every later success body so the batch never blows the context window.
389
377
  let running = 0;
390
378
  let truncatedAny = false;
391
379
  let exhausted = false;
@@ -438,9 +426,8 @@ export function compactMessageRow(m) {
438
426
  };
439
427
  }
440
428
  export function registerGmailTools(server, deps = {}) {
441
- // Per-registry, LIVE account enum + injectable client (S1.10): the
442
- // schema follows the registry's account view at parse time, and the
443
- // custody path is the context's, not the process global.
429
+ // Per-registry, live account enum + injectable client: the schema follows
430
+ // the registry's account view at parse time, not a process-global default.
444
431
  const accountEnum = accountArgLive(() => server.accountAliases()).optional();
445
432
  const getClientFn = deps.getClientFn ?? getClient;
446
433
  const localFiles = deps.localFiles ?? true;
@@ -450,7 +437,7 @@ export function registerGmailTools(server, deps = {}) {
450
437
  'pass full=true for threadId, to, labelIds and the untruncated snippet.',
451
438
  inputSchema: {
452
439
  account: accountEnum.describe('Google account alias'),
453
- query: z.string().describe('Gmail search syntax, e.g. "from:monaam is:unread"'),
440
+ query: z.string().describe('Gmail search syntax, e.g. "from:alice is:unread"'),
454
441
  maxResults: coerceNumber(z.number().min(1).max(100)).optional()
455
442
  .describe('Max results to return (default: 20, max: 100)'),
456
443
  full: coerceBoolean.optional().describe('Return the full row shape instead of the compact default'),
@@ -494,9 +481,8 @@ export function registerGmailTools(server, deps = {}) {
494
481
  });
495
482
  }
496
483
  }
497
- // Search is a pick-one-message step ~always followed by gmail_read;
498
- // the compact row is the selection signal, the rest was measured burn
499
- // (p90 14.4k chars per call). full=true restores the pre-6.0 shape.
484
+ // Search is a pick-one-message step almost always followed by gmail_read;
485
+ // the compact row is the selection signal (full=true returns the previous, heavier row).
500
486
  const rows = full === true ? results : results.map(compactMessageRow);
501
487
  return {
502
488
  content: [{ type: 'text', text: JSON.stringify(rows, null, 2) }],
@@ -64,7 +64,7 @@ function describeMethod(m) {
64
64
  };
65
65
  }
66
66
  export function registerEscapeTools(registry, policy, deps = {}) {
67
- // Live per-registry account validation (S1.10), same as the curated files.
67
+ // Live per-registry account validation, same as the curated files.
68
68
  const accountEnum = accountArgLive(() => registry.accountAliases()).optional();
69
69
  const getClientFn = deps.getClientFn ?? getClient;
70
70
  const toolsets = deps.toolsets ?? getToolsets();
@@ -178,9 +178,8 @@ export function registerEscapeTools(registry, policy, deps = {}) {
178
178
  }
179
179
  let method = index.find((m) => m.id === methodId);
180
180
  if (!method) {
181
- // Some discovery docs keep a legacy id prefix (the searchconsole doc's
182
- // methods are webmasters.*): when the caller prefixed with our api
183
- // alias, retry under the doc's own prefix before failing.
181
+ // Some discovery docs keep a legacy id prefix (searchconsole's methods are
182
+ // webmasters.*): retry under the doc's own prefix if the caller used our api alias.
184
183
  const docPrefix = index[0]?.id.split('.')[0];
185
184
  const [head, ...rest] = String(methodId).split('.');
186
185
  if (docPrefix && (head === apiKey || head === api) && head !== docPrefix && rest.length > 0) {
@@ -205,9 +204,8 @@ export function registerEscapeTools(registry, policy, deps = {}) {
205
204
  const cud = cudFromMethod(method);
206
205
  const policyService = serviceForAlias(apiKey);
207
206
  const lastSegment = method.id.split('.').pop() ?? method.id;
208
- // The escape hatch self-gates, so it needs the same scope signal the
209
- // registry wrapper gets; without it a privileged method would slip past
210
- // safe-writes here while its generated twin is refused.
207
+ // The escape hatch self-gates, so it needs the same scope signal as the registry
208
+ // wrapper, or a privileged method would slip past safe-writes here while its generated twin is refused.
211
209
  const toolRef = { name: `${policyService}_${lastSegment}`, service: policyService, cud, scopes: method.scopes };
212
210
  if (cud !== 'read' && !isAllowed(toolRef, policy)) {
213
211
  return writeDisabledResult(toolRef, policy, account);
@@ -4,9 +4,8 @@ import { accountArgLive } from '../accounts.js';
4
4
  import { getClient } from '../client.js';
5
5
  import { handleGoogleApiError } from './_errors.js';
6
6
  export function registerMeetTools(server, deps = {}) {
7
- // Per-registry, LIVE account enum + injectable client (S1.10): the
8
- // schema follows the registry's account view at parse time, and the
9
- // custody path is the context's, not the process global.
7
+ // Per-registry, live account enum + injectable client: the schema follows
8
+ // the registry's account view at parse time, not a process-global default.
10
9
  const accountEnum = accountArgLive(() => server.accountAliases()).optional();
11
10
  const getClientFn = deps.getClientFn ?? getClient;
12
11
  // ─── Conference records (past meetings) ────────────────────────────────
@@ -6,9 +6,8 @@ import { accountArgLive } from '../accounts.js';
6
6
  import { getClient } from '../client.js';
7
7
  import { handleGoogleApiError } from './_errors.js';
8
8
  export function registerSearchConsoleTools(server, deps = {}) {
9
- // Per-registry, LIVE account enum + injectable client (S1.10): the
10
- // schema follows the registry's account view at parse time, and the
11
- // custody path is the context's, not the process global.
9
+ // Per-registry, live account enum + injectable client: the schema follows
10
+ // the registry's account view at parse time, not a process-global default.
12
11
  const accountEnum = accountArgLive(() => server.accountAliases()).optional();
13
12
  const getClientFn = deps.getClientFn ?? getClient;
14
13
  // ─── Sites ───────────────────────────────────────────────
@@ -5,9 +5,8 @@ import { accountArgLive } from '../accounts.js';
5
5
  import { getClient } from '../client.js';
6
6
  import { handleGoogleApiError, invalidParams } from './_errors.js';
7
7
  export function registerSheetsTools(server, deps = {}) {
8
- // Per-registry, LIVE account enum + injectable client (S1.10): the
9
- // schema follows the registry's account view at parse time, and the
10
- // custody path is the context's, not the process global.
8
+ // Per-registry, live account enum + injectable client: the schema follows
9
+ // the registry's account view at parse time, not a process-global default.
11
10
  const accountEnum = accountArgLive(() => server.accountAliases()).optional();
12
11
  const getClientFn = deps.getClientFn ?? getClient;
13
12
  server.registerTool('sheets_create', {
@@ -7,9 +7,8 @@ import { coerceArray, coerceBoolean, coerceJson } from './_coerce.js';
7
7
  import { sliceClean } from '../trim.js';
8
8
  const SLIDE_TEXT_DIGEST_CHARS = 200;
9
9
  export function registerSlidesTools(server, deps = {}) {
10
- // Per-registry, LIVE account enum + injectable client (S1.10): the
11
- // schema follows the registry's account view at parse time, and the
12
- // custody path is the context's, not the process global.
10
+ // Per-registry, live account enum + injectable client: the schema follows
11
+ // the registry's account view at parse time, not a process-global default.
13
12
  const accountEnum = accountArgLive(() => server.accountAliases()).optional();
14
13
  const getClientFn = deps.getClientFn ?? getClient;
15
14
  server.registerTool('slides_create', {
@@ -5,9 +5,8 @@ import { accountArgLive } from '../accounts.js';
5
5
  import { getClient } from '../client.js';
6
6
  import { handleGoogleApiError, invalidParams } from './_errors.js';
7
7
  export function registerTasksTools(server, deps = {}) {
8
- // Per-registry, LIVE account enum + injectable client (S1.10): the
9
- // schema follows the registry's account view at parse time, and the
10
- // custody path is the context's, not the process global.
8
+ // Per-registry, live account enum + injectable client: the schema follows
9
+ // the registry's account view at parse time, not a process-global default.
11
10
  const accountEnum = accountArgLive(() => server.accountAliases()).optional();
12
11
  const getClientFn = deps.getClientFn ?? getClient;
13
12
  // ─── Tasklists ─────────────────────────────────────────────────────────
package/dist/trim.js CHANGED
@@ -11,9 +11,8 @@ export function sliceClean(text, max) {
11
11
  const sliced = text.slice(0, max);
12
12
  return /[\uD800-\uDBFF]$/.test(sliced) ? sliced.slice(0, -1) : sliced;
13
13
  }
14
- // C0, DEL and C1 (NEL, 8-bit CSI and OSC) can end a line or drive a terminal;
15
- // the line and paragraph separators end a line for many readers; the bidi and
16
- // invisible format controls make a line read differently from its bytes.
14
+ // C0/C1/DEL can end a line or drive a terminal; bidi/invisible format
15
+ // controls make a line read differently from its bytes.
17
16
  const LOG_UNSAFE = /[\p{Cc}\u061c\u200e\u200f\u202a-\u202e\u2028\u2029\u2066-\u2069]/gu;
18
17
  /** An untrusted value made fit for one log line: never throws, at most `max`
19
18
  * characters of it (then `...(len=N)`), each unsafe character as a literal
@@ -4,14 +4,8 @@ import * as os from 'node:os';
4
4
  import { randomBytes } from 'node:crypto';
5
5
  import { performance } from 'node:perf_hooks';
6
6
  import { withFileLock, atomicWriteFileSync } from './fs-atomic.js';
7
- // Local usage metrics (metrics-feature-spec.md). Default OFF at every layer;
8
- // when off, nothing here initializes and the dispatch chain carries no wrapper.
9
- // Zero network egress is structural: this module's import set is a tested
10
- // allowlist (fs/path/os/crypto/perf_hooks/fs-atomic), so egress code cannot
11
- // even be expressed here. Every string persisted is a member of a server-owned
12
- // closed vocabulary, matches a pinned shape regex, or is other/unknown_*/
13
- // _overflow; free text is unrepresentable in the file format. No identity
14
- // dimension of any kind: no aliases, hashes, emails, or exact account counts.
7
+ // Local usage metrics, default OFF; zero network egress is structural (a
8
+ // tested import allowlist). See docs/usage-metrics.md.
15
9
  export const METRICS_DIR_NAME = 'metrics';
16
10
  const FLUSH_MS = 60_000;
17
11
  const RETRY_WINDOW_MS = 10 * 60_000;
@@ -26,7 +20,7 @@ const WRITE_FAILURE_LIMIT = 3;
26
20
  const LAT_BOUNDS = [100, 250, 500, 1000, 2500, 5000, 10000, 30000];
27
21
  const CHARS_BOUNDS = [64, 256, 1024, 4096, 16384, 65536];
28
22
  // methodId backstop shape gate: even a tampered discovery cache cannot turn
29
- // arbitrary strings into recorded values (spec section 1).
23
+ // arbitrary strings into recorded values.
30
24
  const METHOD_ID_RE = /^[a-z][a-zA-Z0-9]*(\.[a-zA-Z0-9]+)+$/;
31
25
  const METHOD_ID_MAX = 128;
32
26
  const TOOL_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
@@ -366,9 +360,8 @@ export class Metrics {
366
360
  f.calls += 1;
367
361
  addInto(f, fan);
368
362
  }
369
- // retry self-correction chain (spec section 1): in-memory only. Both
370
- // chains link consecutive calls, so they only run when one caller owns
371
- // the process (sequencingEnabled).
363
+ // retry self-correction chain, in-memory only; links consecutive calls,
364
+ // so it only runs when one caller owns the process (sequencingEnabled).
372
365
  const nowM = this.mono();
373
366
  if (this.sequencing) {
374
367
  const prev = this.lastError.get(entry.name);
@@ -7,12 +7,11 @@ export interface Policy {
7
7
  allow: string[];
8
8
  deny: string[];
9
9
  /**
10
- * B14 reserved seam (cc-write-control "HTTP posture", LOCKED to OQ-5
11
- * "annotations only"): the dispatch transport is threaded into the resolved
12
- * policy for the EE/future seam, but in v6 alpha it does NOT stiffen the
10
+ * Reserved seam (HTTP posture is "annotations only"): the dispatch
11
+ * transport is threaded into the resolved policy for a future seam, but in v6 alpha it does NOT stiffen the
13
12
  * profile or alter any `isAllowed` verdict. "Stricter on HTTP" is achieved
14
13
  * entirely by the `anthropic/requiresUserInteraction` annotation on the
15
- * irreversible set (A12), emitted identically on both transports.
14
+ * irreversible set, emitted identically on both transports.
16
15
  * Optional so existing Policy literals (codegen/tests) need no change;
17
16
  * resolvePolicy always populates it.
18
17
  */
@@ -22,12 +21,9 @@ interface ToolRef {
22
21
  name: string;
23
22
  service: string;
24
23
  cud: Cud;
25
- /** The method's required OAuth scopes, when the caller knows them. Absent
26
- * means "unknown", which is treated as not privileged: a missing scope list
27
- * must not silently widen the gate OR silently break an ordinary write. */
28
24
  scopes?: readonly string[];
29
25
  }
30
- /** The irreversible set (frozen, cc-write-control): real sends leave the
26
+ /** The irreversible set (frozen): real sends leave the
31
27
  * user's (or org's) identity unrecallably; permanent deletes bypass Trash.
32
28
  * Includes the callable GENERATED twins of the curated ops — the surface is
33
29
  * always callable by name, so a prompt-free twin would defeat the layer.
@@ -1,6 +1,6 @@
1
1
  import { hints } from './hint-provider.js';
2
2
  const PROFILES = ['read-only', 'safe-writes', 'full-writes'];
3
- /** The irreversible set (frozen, cc-write-control): real sends leave the
3
+ /** The irreversible set (frozen): real sends leave the
4
4
  * user's (or org's) identity unrecallably; permanent deletes bypass Trash.
5
5
  * Includes the callable GENERATED twins of the curated ops — the surface is
6
6
  * always callable by name, so a prompt-free twin would defeat the layer.
@@ -64,9 +64,8 @@ function firstMatch(tool, globs) {
64
64
  function matchesAny(tool, globs) {
65
65
  return firstMatch(tool, globs) !== undefined;
66
66
  }
67
- /** Scopes that authorize acting on the whole organization, on legal holds, on
68
- * billing, or on deployed code. A write behind one of these is not a "safe"
69
- * write in any useful sense of the word, whatever its HTTP verb. */
67
+ // Scopes that authorize acting on the whole organization, legal holds,
68
+ // billing, or deployed code: not a "safe" write, whatever its HTTP verb.
70
69
  const PRIVILEGED_SCOPE_RE = /\/auth\/(admin\.directory|admin\.datatransfer|cloud-identity|apps\.licensing|apps\.order|apps\.groups\.settings|apps\.groups\.migration|ediscovery|script\.projects|script\.deployments)/;
71
70
  /** Registering a push channel hands this account's activity to an external URL.
72
71
  * It is shaped like a create and behaves like an export, so the verb hides it. */