mcp-google-multi 6.0.0-beta.2 → 6.0.0-beta.3
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.
- package/dist/accounts.d.ts +17 -1
- package/dist/accounts.js +52 -15
- package/dist/api-probe.d.ts +5 -0
- package/dist/api-probe.js +20 -15
- package/dist/arg-normalize.js +6 -5
- package/dist/arg-strict.d.ts +7 -0
- package/dist/arg-strict.js +34 -4
- package/dist/auth.d.ts +4 -3
- package/dist/auth.js +11 -13
- package/dist/boot-gates.d.ts +26 -0
- package/dist/boot-gates.js +38 -0
- package/dist/cimd-cache.d.ts +38 -0
- package/dist/cimd-cache.js +117 -0
- package/dist/client.d.ts +32 -2
- package/dist/client.js +0 -0
- package/dist/compose.d.ts +11 -0
- package/dist/compose.js +109 -0
- package/dist/config-file.d.ts +13 -0
- package/dist/config-file.js +37 -1
- package/dist/discover.js +3 -1
- package/dist/discovery-client.d.ts +3 -0
- package/dist/discovery-client.js +25 -2
- package/dist/doctor.d.ts +6 -2
- package/dist/doctor.js +119 -23
- package/dist/executor.d.ts +3 -0
- package/dist/executor.js +4 -2
- package/dist/fanout.d.ts +9 -1
- package/dist/fanout.js +44 -10
- package/dist/fs-atomic.d.ts +11 -0
- package/dist/fs-atomic.js +219 -28
- package/dist/hint-provider.d.ts +129 -0
- package/dist/hint-provider.js +64 -0
- package/dist/http-transport.d.ts +62 -4
- package/dist/http-transport.js +343 -24
- package/dist/identity.d.ts +4 -1
- package/dist/identity.js +25 -6
- package/dist/index.js +66 -138
- package/dist/master-key.js +13 -5
- package/dist/mcp-token.d.ts +136 -19
- package/dist/mcp-token.js +569 -72
- package/dist/migrate-config.js +2 -2
- package/dist/oauth-as.d.ts +103 -3
- package/dist/oauth-as.js +374 -79
- package/dist/oauth-consent.d.ts +10 -0
- package/dist/oauth-consent.js +16 -0
- package/dist/outbound-allowlist.js +5 -2
- package/dist/reauth-hint.d.ts +1 -1
- package/dist/reauth-hint.js +12 -8
- package/dist/registry-transfer.d.ts +5 -0
- package/dist/registry-transfer.js +65 -5
- package/dist/registry.d.ts +18 -1
- package/dist/registry.js +71 -15
- package/dist/scope-catalog.d.ts +11 -0
- package/dist/scope-catalog.js +23 -6
- package/dist/scope-observability.d.ts +8 -6
- package/dist/scope-observability.js +29 -13
- package/dist/services.d.ts +15 -10
- package/dist/services.js +59 -15
- package/dist/ssrf-guard.d.ts +23 -9
- package/dist/ssrf-guard.js +101 -62
- package/dist/tenant-purge.d.ts +26 -0
- package/dist/tenant-purge.js +67 -0
- package/dist/token-store.d.ts +20 -2
- package/dist/token-store.js +0 -0
- package/dist/tools/_errors.d.ts +3 -0
- package/dist/tools/_errors.js +53 -24
- package/dist/tools/_local-files.d.ts +9 -0
- package/dist/tools/_local-files.js +18 -0
- package/dist/tools/account-wizard.d.ts +10 -1
- package/dist/tools/account-wizard.js +74 -17
- package/dist/tools/accounts-tool.d.ts +7 -0
- package/dist/tools/accounts-tool.js +34 -18
- package/dist/tools/admin.d.ts +10 -1
- package/dist/tools/admin.js +15 -11
- package/dist/tools/analytics.d.ts +10 -1
- package/dist/tools/analytics.js +13 -8
- package/dist/tools/calendar.d.ts +2 -1
- package/dist/tools/calendar.js +18 -14
- package/dist/tools/chat.d.ts +10 -1
- package/dist/tools/chat.js +13 -8
- package/dist/tools/contacts.d.ts +2 -1
- package/dist/tools/contacts.js +17 -13
- package/dist/tools/docs.d.ts +2 -1
- package/dist/tools/docs.js +34 -30
- package/dist/tools/drive.d.ts +2 -1
- package/dist/tools/drive.js +65 -50
- package/dist/tools/forms.d.ts +10 -1
- package/dist/tools/forms.js +19 -12
- package/dist/tools/generated/_shared.d.ts +2 -1
- package/dist/tools/generated/_shared.js +7 -3
- package/dist/tools/generated/admin.d.ts +2 -1
- package/dist/tools/generated/admin.js +265 -265
- package/dist/tools/generated/analytics.d.ts +2 -1
- package/dist/tools/generated/analytics.js +125 -125
- package/dist/tools/generated/appsmarket.d.ts +2 -1
- package/dist/tools/generated/appsmarket.js +5 -5
- package/dist/tools/generated/calendar.d.ts +2 -1
- package/dist/tools/generated/calendar.js +55 -55
- package/dist/tools/generated/chat.d.ts +2 -1
- package/dist/tools/generated/chat.js +95 -95
- package/dist/tools/generated/classroom.d.ts +2 -1
- package/dist/tools/generated/classroom.js +209 -209
- package/dist/tools/generated/cloudidentity.d.ts +2 -1
- package/dist/tools/generated/cloudidentity.js +133 -133
- package/dist/tools/generated/cloudsearch.d.ts +2 -1
- package/dist/tools/generated/cloudsearch.js +99 -99
- package/dist/tools/generated/contacts.d.ts +2 -1
- package/dist/tools/generated/contacts.js +29 -29
- package/dist/tools/generated/drive.d.ts +2 -1
- package/dist/tools/generated/drive.js +71 -71
- package/dist/tools/generated/driveactivity.d.ts +2 -1
- package/dist/tools/generated/driveactivity.js +3 -3
- package/dist/tools/generated/drivelabels.d.ts +2 -1
- package/dist/tools/generated/drivelabels.js +53 -53
- package/dist/tools/generated/forms.d.ts +2 -1
- package/dist/tools/generated/forms.js +7 -7
- package/dist/tools/generated/gmail.d.ts +2 -1
- package/dist/tools/generated/gmail.js +117 -117
- package/dist/tools/generated/groupsmigration.d.ts +2 -1
- package/dist/tools/generated/groupsmigration.js +3 -3
- package/dist/tools/generated/groupssettings.d.ts +2 -1
- package/dist/tools/generated/groupssettings.js +7 -7
- package/dist/tools/generated/index.d.ts +2 -1
- package/dist/tools/generated/keep.d.ts +2 -1
- package/dist/tools/generated/keep.js +15 -15
- package/dist/tools/generated/licensing.d.ts +2 -1
- package/dist/tools/generated/licensing.js +15 -15
- package/dist/tools/generated/meet.d.ts +2 -1
- package/dist/tools/generated/meet.js +27 -27
- package/dist/tools/generated/postmaster.d.ts +2 -1
- package/dist/tools/generated/postmaster.js +9 -9
- package/dist/tools/generated/reseller.d.ts +2 -1
- package/dist/tools/generated/reseller.js +35 -35
- package/dist/tools/generated/script.d.ts +2 -1
- package/dist/tools/generated/script.js +33 -33
- package/dist/tools/generated/searchconsole.d.ts +2 -1
- package/dist/tools/generated/searchconsole.js +3 -3
- package/dist/tools/generated/sheets.d.ts +2 -1
- package/dist/tools/generated/sheets.js +15 -15
- package/dist/tools/generated/tasks.d.ts +2 -1
- package/dist/tools/generated/tasks.js +5 -5
- package/dist/tools/generated/vault.d.ts +2 -1
- package/dist/tools/generated/vault.js +67 -67
- package/dist/tools/generated/workspaceevents.d.ts +2 -1
- package/dist/tools/generated/workspaceevents.js +31 -31
- package/dist/tools/gmail.d.ts +2 -1
- package/dist/tools/gmail.js +45 -33
- package/dist/tools/google-api.d.ts +2 -3
- package/dist/tools/google-api.js +30 -16
- package/dist/tools/meet.d.ts +10 -1
- package/dist/tools/meet.js +13 -9
- package/dist/tools/searchconsole.d.ts +2 -1
- package/dist/tools/searchconsole.js +17 -13
- package/dist/tools/sheets.d.ts +2 -1
- package/dist/tools/sheets.js +36 -32
- package/dist/tools/slides.d.ts +10 -1
- package/dist/tools/slides.js +17 -10
- package/dist/tools/tasks.d.ts +2 -1
- package/dist/tools/tasks.js +19 -15
- package/dist/trim.d.ts +5 -0
- package/dist/trim.js +20 -0
- package/dist/usage-metrics.d.ts +23 -0
- package/dist/usage-metrics.js +65 -47
- package/dist/write-control.js +10 -2
- package/package.json +27 -1
package/dist/doctor.js
CHANGED
|
@@ -2,16 +2,19 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as readline from 'node:readline';
|
|
4
4
|
import { getAccountSet } from './accounts.js';
|
|
5
|
-
import { deriveAccountHealth } from './tools/accounts-tool.js';
|
|
5
|
+
import { accountHealthDepsFor, deriveAccountHealth } from './tools/accounts-tool.js';
|
|
6
|
+
import { isOwnerContext } from './identity.js';
|
|
6
7
|
import { peekMasterKeyProvenance, deleteMasterKeyMaterial } from './master-key.js';
|
|
7
8
|
import { hasToken } from './token-store.js';
|
|
8
9
|
import { configDir, loadConfigFile } from './config-file.js';
|
|
9
10
|
import { envValueSource } from './env-load.js';
|
|
11
|
+
import { reauthHint } from './reauth-hint.js';
|
|
10
12
|
import { describeMetricsDir, resolveUsageMetrics, sourceLabel } from './usage-metrics.js';
|
|
11
|
-
import { probeApiEnablement } from './api-probe.js';
|
|
13
|
+
import { apiProbeDepsFor, probeApiEnablement } from './api-probe.js';
|
|
12
14
|
import { safeMessage, stringifyEnvelope } from './tools/_errors.js';
|
|
13
15
|
import { resolveHttpConfig, HttpConfigError } from './http-config.js';
|
|
14
16
|
import { parseOwnerEmails } from './http-transport.js';
|
|
17
|
+
import { hints } from './hint-provider.js';
|
|
15
18
|
const MIN_NODE_MAJOR = 22;
|
|
16
19
|
const DEFAULT_DEPS = {
|
|
17
20
|
nodeVersion: process.versions.node,
|
|
@@ -68,9 +71,22 @@ export function apiEnableLink(api) {
|
|
|
68
71
|
return `https://console.cloud.google.com/apis/library/${api}.googleapis.com`;
|
|
69
72
|
}
|
|
70
73
|
const LEGACY_ENV_KEYS = ['GOOGLE_ACCOUNTS', 'GOOGLE_OPTIONAL_SCOPES', 'GOOGLE_ADMIN_ACCOUNTS'];
|
|
71
|
-
function sectionRuntime(deps) {
|
|
74
|
+
function sectionRuntime(deps, tenantScope = false) {
|
|
72
75
|
const major = Number.parseInt(deps.nodeVersion.split('.')[0] ?? '0', 10);
|
|
73
76
|
if (!Number.isFinite(major) || major < MIN_NODE_MAJOR) {
|
|
77
|
+
if (tenantScope) {
|
|
78
|
+
// The host version is operator infrastructure, not the tenant's (E7
|
|
79
|
+
// residue): no version string, and the fix is the operator's to apply.
|
|
80
|
+
const rendered = hints()?.render({ kind: 'operator_only', slug: 'E_NODE_TOO_OLD' });
|
|
81
|
+
return {
|
|
82
|
+
id: 1,
|
|
83
|
+
title: 'Runtime',
|
|
84
|
+
verdict: 'fail',
|
|
85
|
+
slug: 'E_NODE_TOO_OLD',
|
|
86
|
+
lines: ['Runtime below the supported version'],
|
|
87
|
+
hint: rendered?.hint ?? 'Tell the server operator the code E_NODE_TOO_OLD.',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
74
90
|
return {
|
|
75
91
|
id: 1,
|
|
76
92
|
title: 'Runtime',
|
|
@@ -80,23 +96,36 @@ function sectionRuntime(deps) {
|
|
|
80
96
|
hint: `Upgrade to Node ${MIN_NODE_MAJOR} LTS or newer.`,
|
|
81
97
|
};
|
|
82
98
|
}
|
|
83
|
-
return {
|
|
99
|
+
return {
|
|
100
|
+
id: 1,
|
|
101
|
+
title: 'Runtime',
|
|
102
|
+
verdict: 'ok',
|
|
103
|
+
lines: [tenantScope ? 'Runtime ok' : `Node.js ${deps.nodeVersion} (>= ${MIN_NODE_MAJOR})`],
|
|
104
|
+
};
|
|
84
105
|
}
|
|
85
|
-
function sectionConfig(deps, set) {
|
|
106
|
+
function sectionConfig(deps, set, tenantScope = false) {
|
|
86
107
|
const lines = [];
|
|
87
108
|
let verdict = 'ok';
|
|
88
109
|
let hint;
|
|
89
110
|
let slug;
|
|
90
111
|
if (!set || set.aliases.length === 0) {
|
|
112
|
+
const tenantHint = tenantScope ? (hints()?.render({ kind: 'no_accounts' })?.hint ?? '') : undefined;
|
|
91
113
|
return {
|
|
92
114
|
id: 2,
|
|
93
115
|
title: 'Config',
|
|
94
116
|
verdict: 'fail',
|
|
95
117
|
slug: 'E_NO_ACCOUNTS_CONFIGURED',
|
|
96
118
|
lines: ['No accounts configured.'],
|
|
97
|
-
|
|
119
|
+
...(tenantScope
|
|
120
|
+
? (tenantHint ? { hint: tenantHint } : {})
|
|
121
|
+
: { hint: 'Add an account: run `npx mcp-google-multi migrate-config` or set GOOGLE_ACCOUNTS.' }),
|
|
98
122
|
};
|
|
99
123
|
}
|
|
124
|
+
// The config file, process env, working directory and metrics store below
|
|
125
|
+
// are the operator's host, not the context's: a tenant sees its own count.
|
|
126
|
+
if (tenantScope) {
|
|
127
|
+
return { id: 2, title: 'Config', verdict: 'ok', lines: [`${set.aliases.length} account(s) configured`] };
|
|
128
|
+
}
|
|
100
129
|
const cfgPath = path.join(configDir(), 'config.json');
|
|
101
130
|
lines.push(deps.fileExists(cfgPath) ? `config.json present (${set.aliases.length} account(s))` : `config.json absent — accounts sourced from env (${set.aliases.length})`);
|
|
102
131
|
const legacyEnv = LEGACY_ENV_KEYS.filter((k) => deps.env[k]);
|
|
@@ -181,7 +210,8 @@ function sectionsTokensAndScopes(deps, aliases) {
|
|
|
181
210
|
if (scopeVerdict === 'ok')
|
|
182
211
|
scopeVerdict = 'warn';
|
|
183
212
|
scopeLines.push(`${alias}: ${r.callable.length} callable, ${r.requestable.length} requested-not-granted`);
|
|
184
|
-
|
|
213
|
+
const rendered = hints()?.render({ kind: 'scopes_missing', account: alias });
|
|
214
|
+
scopeHint = rendered?.hint ?? `Re-auth to grant missing scopes. ${reauthHint(alias)}`;
|
|
185
215
|
}
|
|
186
216
|
else {
|
|
187
217
|
scopeLines.push(`${alias}: ${r.callable.length} callable, all profile scopes granted`);
|
|
@@ -192,7 +222,7 @@ function sectionsTokensAndScopes(deps, aliases) {
|
|
|
192
222
|
{ id: 5, title: 'Scopes', verdict: scopeVerdict, lines: scopeLines, ...(scopeHint ? { hint: scopeHint } : {}), ...(scopeVerdict === 'warn' ? { slug: 'E_SCOPE_NOT_GRANTED' } : {}) },
|
|
193
223
|
];
|
|
194
224
|
}
|
|
195
|
-
async function sectionApiEnablement(deps, aliases) {
|
|
225
|
+
async function sectionApiEnablement(deps, aliases, tenantScope = false) {
|
|
196
226
|
if (!deps.probeApi) {
|
|
197
227
|
return { id: 6, title: 'API enablement', verdict: 'unknown', lines: ['Probe not run (no live account probe configured).'] };
|
|
198
228
|
}
|
|
@@ -209,8 +239,15 @@ async function sectionApiEnablement(deps, aliases) {
|
|
|
209
239
|
results = await deps.probeApi(healthy);
|
|
210
240
|
}
|
|
211
241
|
catch (e) {
|
|
212
|
-
// Network / transient: WARN with the target, never crash the report.
|
|
213
|
-
|
|
242
|
+
// Network / transient: WARN with the target, never crash the report. The
|
|
243
|
+
// raw text can name an egress or proxy host (E7 residue): tenant scope
|
|
244
|
+
// drops it.
|
|
245
|
+
return {
|
|
246
|
+
id: 6,
|
|
247
|
+
title: 'API enablement',
|
|
248
|
+
verdict: 'warn',
|
|
249
|
+
lines: [tenantScope ? 'Probe could not complete.' : `Probe could not complete: ${e?.message ?? e}`],
|
|
250
|
+
};
|
|
214
251
|
}
|
|
215
252
|
if (results.length === 0) {
|
|
216
253
|
return { id: 6, title: 'API enablement', verdict: 'unknown', lines: [`No probeable service scopes granted on "${healthy}".`] };
|
|
@@ -218,13 +255,14 @@ async function sectionApiEnablement(deps, aliases) {
|
|
|
218
255
|
const disabled = results.filter((r) => r.notEnabled);
|
|
219
256
|
const lines = results.map((r) => `${r.service}: ${r.ok ? 'enabled' : r.notEnabled ? 'NOT ENABLED' : `unknown (${r.message ?? 'error'})`}`);
|
|
220
257
|
if (disabled.length > 0) {
|
|
258
|
+
const rendered = hints()?.render({ kind: 'apis_disabled', services: disabled.map((r) => ({ service: r.service, api: r.api })) });
|
|
221
259
|
return {
|
|
222
260
|
id: 6,
|
|
223
261
|
title: 'API enablement',
|
|
224
262
|
verdict: 'fail',
|
|
225
263
|
slug: 'E_API_NOT_ENABLED',
|
|
226
264
|
lines,
|
|
227
|
-
hint: disabled.map((r) => `Enable ${r.service}: ${apiEnableLink(r.api)}`).join('\n'),
|
|
265
|
+
hint: rendered?.hint ?? disabled.map((r) => `Enable ${r.service}: ${apiEnableLink(r.api)}`).join('\n'),
|
|
228
266
|
};
|
|
229
267
|
}
|
|
230
268
|
return { id: 6, title: 'API enablement', verdict: 'ok', lines: lines.length ? lines : ['(probed account, all enabled)'] };
|
|
@@ -299,21 +337,29 @@ export function overallVerdict(sections) {
|
|
|
299
337
|
}
|
|
300
338
|
return worst;
|
|
301
339
|
}
|
|
302
|
-
export async function runDiagnostics(deps = DEFAULT_DEPS) {
|
|
340
|
+
export async function runDiagnostics(deps = DEFAULT_DEPS, opts = {}) {
|
|
341
|
+
// Tenant scope omits the OPERATOR sections: §3 (key provenance = box
|
|
342
|
+
// infrastructure) and §7 (its owner-gate lines print MCP_OWNER_EMAILS
|
|
343
|
+
// values — other people's addresses from a tenant's seat), and trims §2 to
|
|
344
|
+
// the context's own account count.
|
|
345
|
+
const tenantScope = opts.scope === 'tenant';
|
|
303
346
|
const sections = [];
|
|
304
|
-
sections.push(sectionRuntime(deps));
|
|
347
|
+
sections.push(sectionRuntime(deps, tenantScope));
|
|
305
348
|
const set = deps.accountSet();
|
|
306
|
-
sections.push(sectionConfig(deps, set));
|
|
349
|
+
sections.push(sectionConfig(deps, set, tenantScope));
|
|
307
350
|
const aliases = set?.aliases ?? [];
|
|
308
|
-
|
|
351
|
+
if (!tenantScope)
|
|
352
|
+
sections.push(sectionKeys(deps, aliases));
|
|
309
353
|
if (aliases.length > 0) {
|
|
310
354
|
const [tokens, scopes] = sectionsTokensAndScopes(deps, aliases);
|
|
311
355
|
sections.push(tokens, scopes);
|
|
312
|
-
sections.push(await sectionApiEnablement(deps, aliases));
|
|
356
|
+
sections.push(await sectionApiEnablement(deps, aliases, tenantScope));
|
|
357
|
+
}
|
|
358
|
+
if (!tenantScope) {
|
|
359
|
+
const http = await sectionHttp(deps, aliases);
|
|
360
|
+
if (http)
|
|
361
|
+
sections.push(http);
|
|
313
362
|
}
|
|
314
|
-
const http = await sectionHttp(deps, aliases);
|
|
315
|
-
if (http)
|
|
316
|
-
sections.push(http);
|
|
317
363
|
return { verdict: overallVerdict(sections), sections };
|
|
318
364
|
}
|
|
319
365
|
// --- skins -----------------------------------------------------------------
|
|
@@ -333,9 +379,35 @@ export function renderDoctorText(report) {
|
|
|
333
379
|
out.push(`Overall: ${report.verdict.toUpperCase()}`);
|
|
334
380
|
return out.join('\n');
|
|
335
381
|
}
|
|
336
|
-
|
|
382
|
+
const EMAIL_LOCAL_CHAR = /[A-Za-z0-9._%+-]/;
|
|
383
|
+
const EMAIL_DOMAIN_CHAR = /[A-Za-z0-9.-]/;
|
|
384
|
+
/** Mask the local-part of an email so a report never carries a full address
|
|
385
|
+
* (BR8). A linear scan, not a regex: the `local* @domain` pattern backtracks
|
|
386
|
+
* polynomially on long local-charset runs without an @ (CodeQL
|
|
387
|
+
* js/polynomial-redos), and report text can embed arbitrary error strings. */
|
|
337
388
|
function maskEmails(text) {
|
|
338
|
-
|
|
389
|
+
let out = '';
|
|
390
|
+
let i = 0;
|
|
391
|
+
while (i < text.length) {
|
|
392
|
+
if (text[i] === '@') {
|
|
393
|
+
let j = i + 1;
|
|
394
|
+
while (j < text.length && EMAIL_DOMAIN_CHAR.test(text[j]))
|
|
395
|
+
j += 1;
|
|
396
|
+
let k = out.length;
|
|
397
|
+
while (k > 0 && EMAIL_LOCAL_CHAR.test(out[k - 1]))
|
|
398
|
+
k -= 1;
|
|
399
|
+
if (j > i + 1 && k < out.length) {
|
|
400
|
+
// keep the first local char, mask the rest ('*' is outside the local
|
|
401
|
+
// charset, so a masked run can never be re-masked by a later @)
|
|
402
|
+
out = `${out.slice(0, k + 1)}***${text.slice(i, j)}`;
|
|
403
|
+
i = j;
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
out += text[i];
|
|
408
|
+
i += 1;
|
|
409
|
+
}
|
|
410
|
+
return out;
|
|
339
411
|
}
|
|
340
412
|
/** Redacted, paste-ready bug report (BR8): verdicts + provenance labels + token
|
|
341
413
|
* statuses + slugs; NO token values, secrets, keys, or full email addresses. */
|
|
@@ -356,6 +428,26 @@ export function exitCodeFor(report, strict) {
|
|
|
356
428
|
return 1;
|
|
357
429
|
return 0;
|
|
358
430
|
}
|
|
431
|
+
/** The engine's account, token and probe reads bound to one context, so a
|
|
432
|
+
* report never shows (or probes with) another context's accounts. */
|
|
433
|
+
function diagnosticsDepsFor(ctx) {
|
|
434
|
+
const health = accountHealthDepsFor(ctx.tokenStore, () => ctx.accounts);
|
|
435
|
+
const probe = apiProbeDepsFor(ctx.tokenStore.readToken, ctx.getClient);
|
|
436
|
+
return {
|
|
437
|
+
...DEFAULT_DEPS,
|
|
438
|
+
accountSet: () => {
|
|
439
|
+
try {
|
|
440
|
+
return ctx.accounts;
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
accountHealth: (alias) => deriveAccountHealth(alias, health),
|
|
447
|
+
anyTokensExist: (aliases) => aliases.some((a) => ctx.tokenStore.hasToken(a)),
|
|
448
|
+
probeApi: (alias) => probeApiEnablement(alias, probe),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
359
451
|
/** Agent-callable structured health report (read-only). Mirrors `doctor`'s
|
|
360
452
|
* engine. Registered as a META tool, like `account_list`: it introspects this
|
|
361
453
|
* server rather than Google data, and as a normal tool it became a service of
|
|
@@ -363,14 +455,18 @@ export function exitCodeFor(report, strict) {
|
|
|
363
455
|
* from already-registered services, and this runs after that), so it was
|
|
364
456
|
* advertised only once something else expanded the surface. The README sends
|
|
365
457
|
* people here when they are stuck, so it has to be findable. */
|
|
366
|
-
export function registerDiagnoseTool(registry) {
|
|
458
|
+
export function registerDiagnoseTool(registry, ctx) {
|
|
459
|
+
// A non-owner context gets the tenant-scoped report; only the owner context
|
|
460
|
+
// (or the context-free CLI surface) keeps the full operator report.
|
|
461
|
+
const scope = ctx && !isOwnerContext(ctx) ? 'tenant' : 'operator';
|
|
462
|
+
const deps = ctx ? diagnosticsDepsFor(ctx) : DEFAULT_DEPS;
|
|
367
463
|
registry.registerMeta('diagnose', {
|
|
368
464
|
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
369
465
|
description: 'Health report for this server: runtime, config, keys, per-account token status, scope grants, and API enablement. Read-only; returns copy-pasteable fixes for anything wrong. Call this to diagnose auth/config failures.',
|
|
370
466
|
inputSchema: {},
|
|
371
467
|
}, async () => {
|
|
372
468
|
try {
|
|
373
|
-
const result = await runDiagnostics();
|
|
469
|
+
const result = await runDiagnostics(deps, { scope });
|
|
374
470
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
375
471
|
}
|
|
376
472
|
catch (e) {
|
package/dist/executor.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getClient } from './client.js';
|
|
2
|
+
import { type ScopeSetDeps } from './scope-observability.js';
|
|
2
3
|
export declare const MAX_RESPONSE_CHARS = 100000;
|
|
3
4
|
export type QueryValue = string | number | boolean;
|
|
4
5
|
export type QueryParams = Record<string, QueryValue | QueryValue[]>;
|
|
@@ -13,6 +14,8 @@ export interface ApiMethodRef {
|
|
|
13
14
|
}
|
|
14
15
|
export interface ExecuteDeps {
|
|
15
16
|
getClientFn?: typeof getClient;
|
|
17
|
+
/** Token + profile view behind a 403 scope hint; absent = the single owner. */
|
|
18
|
+
scopeDeps?: ScopeSetDeps;
|
|
16
19
|
}
|
|
17
20
|
export interface ExecuteArgs {
|
|
18
21
|
account: string;
|
package/dist/executor.js
CHANGED
|
@@ -4,6 +4,7 @@ import { expandPath, isGoogleApiUrl } from './discovery-client.js';
|
|
|
4
4
|
import { handleGoogleApiError } from './tools/_errors.js';
|
|
5
5
|
import { scopeHintForMethod } from './scope-observability.js';
|
|
6
6
|
import { checkOutboundForMethod } from './outbound-allowlist.js';
|
|
7
|
+
import { hints } from './hint-provider.js';
|
|
7
8
|
export const MAX_RESPONSE_CHARS = 100_000;
|
|
8
9
|
export function jsonResult(payload, isError = false) {
|
|
9
10
|
const base = { content: [{ type: 'text', text: JSON.stringify(payload) }] };
|
|
@@ -70,10 +71,11 @@ export async function executeApiMethod(method, args, deps = {}) {
|
|
|
70
71
|
}, true);
|
|
71
72
|
}
|
|
72
73
|
if (!isGoogleApiUrl(url)) {
|
|
74
|
+
const rendered = hints()?.render({ kind: 'untrusted_host' });
|
|
73
75
|
return jsonResult({
|
|
74
76
|
error: 'untrusted_host',
|
|
75
77
|
message: `Refusing to send credentials to a non-googleapis.com host for "${method.id}".`,
|
|
76
|
-
hint: 'The discovery cache may be corrupt; delete DISCOVERY_CACHE_PATH and retry.',
|
|
78
|
+
hint: rendered?.hint ?? 'The discovery cache may be corrupt; delete DISCOVERY_CACHE_PATH and retry.',
|
|
77
79
|
retriable: false,
|
|
78
80
|
account: args.account,
|
|
79
81
|
}, true);
|
|
@@ -103,6 +105,6 @@ export async function executeApiMethod(method, args, deps = {}) {
|
|
|
103
105
|
return { content: [{ type: 'text', text }] };
|
|
104
106
|
}
|
|
105
107
|
catch (error) {
|
|
106
|
-
return handleGoogleApiError(error, args.account, undefined, () => method.scopes?.length ? scopeHintForMethod(method.scopes, args.account) : null);
|
|
108
|
+
return handleGoogleApiError(error, args.account, undefined, () => method.scopes?.length ? scopeHintForMethod(method.scopes, args.account, deps.scopeDeps) : null);
|
|
107
109
|
}
|
|
108
110
|
}
|
package/dist/fanout.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const CSV_RE: RegExp;
|
|
3
|
-
export declare function fanoutAccountField(description: string): z.ZodType;
|
|
3
|
+
export declare function fanoutAccountField(description: string, aliases: () => readonly string[]): z.ZodType;
|
|
4
|
+
/** '*' over an empty account set would fan out to nothing and report success. */
|
|
5
|
+
export declare function noAccountsResult(): {
|
|
6
|
+
content: {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}[];
|
|
10
|
+
isError: true;
|
|
11
|
+
};
|
|
4
12
|
export type AccountSelector = {
|
|
5
13
|
ok: true;
|
|
6
14
|
fanout: boolean;
|
package/dist/fanout.js
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { getAccountSet, liveAccountCheck, unknownAliasMessage } from './accounts.js';
|
|
3
|
+
import { echoName, echoNames, MAX_SUGGESTED_KEYS } from './arg-strict.js';
|
|
4
|
+
import { hints } from './hint-provider.js';
|
|
3
5
|
export const CSV_RE = /^[a-zA-Z0-9_-]+(\s*,\s*[a-zA-Z0-9_-]+)+$/;
|
|
4
6
|
const FANOUT_CONCURRENCY = 5;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
// `aliases` is required (no global default): the field is baked into a
|
|
8
|
+
// registry's inputSchema, so it must read THAT registry's account set or a
|
|
9
|
+
// '*' would advertise and accept another registry's aliases. It is a getter:
|
|
10
|
+
// the set is read at parse time, so an alias added mid-session (account_add,
|
|
11
|
+
// a tenant link) is valid on tools registered before it existed.
|
|
12
|
+
export function fanoutAccountField(description, aliases) {
|
|
13
|
+
const boot = aliases();
|
|
14
|
+
const csvExample = boot.length > 1 ? `; or a CSV subset like "${boot.slice(0, 2).join(',')}"` : '';
|
|
7
15
|
// The union's OWN error only fires when every branch aborts at the type
|
|
8
16
|
// check (a non-string). For a string, zod surfaces the single non-aborted
|
|
9
17
|
// branch verbatim, which is the CSV regex, so a mistyped alias used to read
|
|
10
18
|
// as "Invalid string: must match pattern /^[a-zA-Z0-9_-]+(,...)/" and never
|
|
11
19
|
// named an alias. Hence the same message on BOTH.
|
|
12
|
-
const bad = () => unknownAliasMessage(
|
|
20
|
+
const bad = () => unknownAliasMessage(aliases(), true);
|
|
13
21
|
return z
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
.union([z.
|
|
22
|
+
// The selector branch stays first: tools/list injects the live
|
|
23
|
+
// '*' + alias enum into it (registry withLiveAccountEnum).
|
|
24
|
+
.union([z.string().check(liveAccountCheck(aliases, true)), z.string().regex(CSV_RE, { error: bad })], { error: bad })
|
|
17
25
|
.optional()
|
|
18
26
|
.describe(`${description}; "*" = all accounts${csvExample}; omit for the default account`);
|
|
19
27
|
}
|
|
28
|
+
/** '*' over an empty account set would fan out to nothing and report success. */
|
|
29
|
+
export function noAccountsResult() {
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: 'text',
|
|
34
|
+
text: JSON.stringify({
|
|
35
|
+
error: 'validation_error',
|
|
36
|
+
message: 'No Google account is configured, so "*" selects nothing.',
|
|
37
|
+
hint: unknownAliasMessage([]),
|
|
38
|
+
retriable: false,
|
|
39
|
+
account: '*',
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
20
46
|
export function parseAccountSelector(value, accounts = getAccountSet().aliases) {
|
|
21
47
|
if (value === '*')
|
|
22
48
|
return { ok: true, fanout: true, aliases: [...accounts] };
|
|
@@ -53,10 +79,11 @@ export function invalidAccountsResult(invalid, accounts = getAccountSet().aliase
|
|
|
53
79
|
type: 'text',
|
|
54
80
|
text: JSON.stringify({
|
|
55
81
|
error: 'validation_error',
|
|
56
|
-
|
|
82
|
+
// The caller's CSV can hold any number of tokens: echo a prefix.
|
|
83
|
+
message: `Unknown account alias(es): ${echoNames(invalid)}.`,
|
|
57
84
|
hint: unknownAliasMessage(accounts, true),
|
|
58
85
|
retriable: false,
|
|
59
|
-
account: invalid.join(','),
|
|
86
|
+
account: invalid.slice(0, MAX_SUGGESTED_KEYS).map(echoName).join(','),
|
|
60
87
|
}),
|
|
61
88
|
},
|
|
62
89
|
],
|
|
@@ -87,10 +114,17 @@ export async function runFanout(handler, args, aliases) {
|
|
|
87
114
|
results[i] = res?.isError ? { account, ok: false, error: payload } : { account, ok: true, data: payload };
|
|
88
115
|
}
|
|
89
116
|
catch (err) {
|
|
117
|
+
const rendered = hints()?.render({ kind: 'internal', account });
|
|
90
118
|
results[i] = {
|
|
91
119
|
account,
|
|
92
120
|
ok: false,
|
|
93
|
-
error: {
|
|
121
|
+
error: {
|
|
122
|
+
error: 'internal',
|
|
123
|
+
message: rendered?.message ?? err.message,
|
|
124
|
+
hint: rendered?.hint,
|
|
125
|
+
retriable: false,
|
|
126
|
+
account,
|
|
127
|
+
},
|
|
94
128
|
};
|
|
95
129
|
}
|
|
96
130
|
}
|
package/dist/fs-atomic.d.ts
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/** Which process instance holds a lock: PID namespace id, start time in
|
|
2
|
+
* clock ticks since boot, and a boot id prefix. A part this process cannot
|
|
3
|
+
* read is `host` or `0` (all three without /proc). */
|
|
4
|
+
interface LockIdentity {
|
|
5
|
+
ns: string;
|
|
6
|
+
start: string;
|
|
7
|
+
boot: string;
|
|
8
|
+
}
|
|
9
|
+
/** Test hook: act as a process with this identity (undefined restores the real one). */
|
|
10
|
+
export declare function __setLockIdentityForTest(identity?: LockIdentity): void;
|
|
1
11
|
export declare function withFileLock<T>(absPath: string, fn: () => T, label?: string): T;
|
|
2
12
|
export declare function atomicWriteFileSync(absPath: string, contents: string, mode?: number): void;
|
|
3
13
|
export declare function atomicWriteWithLock(absPath: string, contents: string, mode?: number): void;
|
|
14
|
+
export {};
|