@trops/dash-core 0.1.560 → 0.1.562
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/electron/index.js +429 -28
- package/dist/electron/index.js.map +1 -1
- package/dist/index.esm.js +54 -10
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +54 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/electron/index.js
CHANGED
|
@@ -2397,9 +2397,27 @@ var mountTokenRegistry = { register: register$1, lookup, unregister: unregister$
|
|
|
2397
2397
|
* Conversion: registry keys are scoped (`@trops/google-drive`); grant
|
|
2398
2398
|
* keys are dotted (`trops.google-drive.GDriveFileList`). The mapping
|
|
2399
2399
|
* for each entry is `@<scope>/<name>` + `<componentName>` →
|
|
2400
|
-
* `<scope>.<name>.<componentName>`.
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
2400
|
+
* `<scope>.<name>.<componentName>`.
|
|
2401
|
+
*
|
|
2402
|
+
* Resolution strategy:
|
|
2403
|
+
*
|
|
2404
|
+
* 1. SCOPED PATH (preferred): a 3-part widgetId carries the package
|
|
2405
|
+
* identity in its first two segments. We rebuild `@scope/pkg` from
|
|
2406
|
+
* those segments and look the package up directly in the registry.
|
|
2407
|
+
* That keeps two packages exporting the same bare component name
|
|
2408
|
+
* from cross-contaminating each other's grant scope.
|
|
2409
|
+
*
|
|
2410
|
+
* 2. LEGACY BARE-NAME FALLBACK: for legacy widget ids that don't
|
|
2411
|
+
* carry scope (1- or 2-segment ids), walk the registry and match
|
|
2412
|
+
* on the last dot-segment. Honours `entry.packageId` when present
|
|
2413
|
+
* so the returned sibling set is correctly mapped.
|
|
2414
|
+
*
|
|
2415
|
+
* The scoped-first strategy was added after a bare-name collision
|
|
2416
|
+
* caused JIT consent for a widget in `@ai-built/prompt-validation` to
|
|
2417
|
+
* resolve siblings under `@trops/google-drive` (both packages
|
|
2418
|
+
* happened to ship a `GoogleDriveRecentFiles` component). The grant
|
|
2419
|
+
* subsequently landed on the wrong package's widgets entirely. Don't
|
|
2420
|
+
* regress this — the test file pins the collision case explicitly.
|
|
2403
2421
|
*/
|
|
2404
2422
|
|
|
2405
2423
|
const FALLBACK = (widgetId) => ({
|
|
@@ -2412,6 +2430,18 @@ function _bareNameFromWidgetId(widgetId) {
|
|
|
2412
2430
|
return idx >= 0 ? widgetId.slice(idx + 1) : widgetId;
|
|
2413
2431
|
}
|
|
2414
2432
|
|
|
2433
|
+
/**
|
|
2434
|
+
* Pull `@scope/pkg` out of a 3-part dotted widgetId. Returns null for
|
|
2435
|
+
* any input that isn't exactly 3 dot-segments — 1- and 2-segment ids
|
|
2436
|
+
* fall through to the legacy bare-name path.
|
|
2437
|
+
*/
|
|
2438
|
+
function _packageIdFromScopedWidgetId(widgetId) {
|
|
2439
|
+
const parts = widgetId.split(".");
|
|
2440
|
+
if (parts.length !== 3) return null;
|
|
2441
|
+
if (!parts[0] || !parts[1] || !parts[2]) return null;
|
|
2442
|
+
return `@${parts[0]}/${parts[1]}`;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2415
2445
|
function _toGrantId(packageId, componentName) {
|
|
2416
2446
|
// "@trops/google-drive" + "GDriveFileList" → "trops.google-drive.GDriveFileList"
|
|
2417
2447
|
// Strip leading "@" and replace the first "/" with ".".
|
|
@@ -2432,6 +2462,26 @@ function _entries(snapshot) {
|
|
|
2432
2462
|
return [];
|
|
2433
2463
|
}
|
|
2434
2464
|
|
|
2465
|
+
/**
|
|
2466
|
+
* Look up an exact registry entry by packageId (either the registry
|
|
2467
|
+
* key OR the entry's own `packageId` field). Returns the entry or
|
|
2468
|
+
* null. We accept both shapes because some registry persistence
|
|
2469
|
+
* paths historically keyed by `name` instead of `packageId`.
|
|
2470
|
+
*/
|
|
2471
|
+
function _findEntryByPackageId(packageId, registrySnapshot) {
|
|
2472
|
+
for (const [key, entry] of _entries(registrySnapshot)) {
|
|
2473
|
+
if (key === packageId) return entry;
|
|
2474
|
+
if (
|
|
2475
|
+
entry &&
|
|
2476
|
+
typeof entry.packageId === "string" &&
|
|
2477
|
+
entry.packageId === packageId
|
|
2478
|
+
) {
|
|
2479
|
+
return entry;
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return null;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2435
2485
|
/**
|
|
2436
2486
|
* @param {string} widgetId — grant-keyed widget id
|
|
2437
2487
|
* @param {Map<string, object>|object} registrySnapshot — getWidgetRegistry().widgets
|
|
@@ -2440,6 +2490,30 @@ function _entries(snapshot) {
|
|
|
2440
2490
|
function resolveSiblings$3(widgetId, registrySnapshot) {
|
|
2441
2491
|
if (typeof widgetId !== "string" || !widgetId) return FALLBACK(widgetId);
|
|
2442
2492
|
|
|
2493
|
+
// SCOPED PATH — the widgetId is already scoped, so we know its
|
|
2494
|
+
// package directly. No cross-package matching, no collision risk.
|
|
2495
|
+
const derivedPackageId = _packageIdFromScopedWidgetId(widgetId);
|
|
2496
|
+
if (derivedPackageId) {
|
|
2497
|
+
const entry = _findEntryByPackageId(derivedPackageId, registrySnapshot);
|
|
2498
|
+
const names = Array.isArray(entry?.componentNames)
|
|
2499
|
+
? entry.componentNames
|
|
2500
|
+
: null;
|
|
2501
|
+
if (entry && names && names.length > 0) {
|
|
2502
|
+
return {
|
|
2503
|
+
packageId: derivedPackageId,
|
|
2504
|
+
siblingWidgetIds: names.map((n) => _toGrantId(derivedPackageId, n)),
|
|
2505
|
+
};
|
|
2506
|
+
}
|
|
2507
|
+
// Package is known by id but the registry entry is missing or
|
|
2508
|
+
// empty (race: package just installed, registry write hasn't
|
|
2509
|
+
// landed). Don't fall through to bare-name search — that would
|
|
2510
|
+
// cross-contaminate by matching another package whose component
|
|
2511
|
+
// happens to share the bare name. Return the single widget.
|
|
2512
|
+
return FALLBACK(widgetId);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// LEGACY BARE-NAME FALLBACK — only for widget ids that don't carry
|
|
2516
|
+
// scope (1- or 2-segment ids).
|
|
2443
2517
|
const bareName = _bareNameFromWidgetId(widgetId);
|
|
2444
2518
|
if (!bareName) return FALLBACK(widgetId);
|
|
2445
2519
|
|
|
@@ -3528,14 +3602,60 @@ function expandHome(p) {
|
|
|
3528
3602
|
/**
|
|
3529
3603
|
* Parse a widget's package.json contents into a normalized permissions
|
|
3530
3604
|
* object. Returns null if no `dash.permissions.mcp` block exists.
|
|
3605
|
+
*
|
|
3606
|
+
* When `componentName` is provided AND the package ships a
|
|
3607
|
+
* `dash.permissions.mcpByComponent` block, the parser returns the
|
|
3608
|
+
* declaration for that specific widget instead of the package-level
|
|
3609
|
+
* union. This is what closes the cross-pollination gap: multi-widget
|
|
3610
|
+
* packages give each widget its own server/tools list, scanned from
|
|
3611
|
+
* the widget's own component file.
|
|
3612
|
+
*
|
|
3613
|
+
* If `componentName` is omitted (or the per-component block doesn't
|
|
3614
|
+
* exist), we fall back to the package-level `mcp` block — the
|
|
3615
|
+
* back-compat path for older packages that haven't been re-scanned.
|
|
3531
3616
|
*/
|
|
3532
|
-
function parseManifestPermissions(packageJson) {
|
|
3617
|
+
function parseManifestPermissions(packageJson, componentName = null) {
|
|
3533
3618
|
if (!packageJson || typeof packageJson !== "object") return null;
|
|
3619
|
+
|
|
3620
|
+
const byComponent = packageJson?.dash?.permissions?.mcpByComponent;
|
|
3621
|
+
const hasByComponent =
|
|
3622
|
+
byComponent &&
|
|
3623
|
+
typeof byComponent === "object" &&
|
|
3624
|
+
Object.keys(byComponent).length > 0;
|
|
3625
|
+
|
|
3626
|
+
// If the package ships a per-component breakdown, that's
|
|
3627
|
+
// authoritative for any widget we're asking about. Widgets not
|
|
3628
|
+
// listed there are widgets the scanner found NO MCP usage in —
|
|
3629
|
+
// they declare nothing, not the package-level union. Falling
|
|
3630
|
+
// back to the package union for an unlisted widget would defeat
|
|
3631
|
+
// the whole point of the breakdown (re-merging every sibling's
|
|
3632
|
+
// tools into each unlisted widget).
|
|
3633
|
+
if (componentName && hasByComponent) {
|
|
3634
|
+
if (byComponent[componentName]) {
|
|
3635
|
+
return _normalizeServers(byComponent[componentName]?.servers);
|
|
3636
|
+
}
|
|
3637
|
+
// Component exists in this package but the scanner found no
|
|
3638
|
+
// MCP usage in its source. Declare nothing.
|
|
3639
|
+
return null;
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
// No per-component data (older package) OR no componentName
|
|
3643
|
+
// supplied by the caller — fall back to the package-level union
|
|
3644
|
+
// for back-compat.
|
|
3534
3645
|
const mcp = packageJson?.dash?.permissions?.mcp;
|
|
3535
3646
|
if (!mcp || typeof mcp !== "object") return null;
|
|
3647
|
+
return _normalizeServers(mcp);
|
|
3648
|
+
}
|
|
3536
3649
|
|
|
3650
|
+
/**
|
|
3651
|
+
* Normalize a `{server: {tools, readPaths, writePaths}}` block into
|
|
3652
|
+
* the standard `{servers: {...}}` shape used by the gate and panel.
|
|
3653
|
+
* Filters non-string entries and expands `~/` paths.
|
|
3654
|
+
*/
|
|
3655
|
+
function _normalizeServers(rawServers) {
|
|
3656
|
+
if (!rawServers || typeof rawServers !== "object") return null;
|
|
3537
3657
|
const servers = {};
|
|
3538
|
-
for (const [serverName, raw] of Object.entries(
|
|
3658
|
+
for (const [serverName, raw] of Object.entries(rawServers)) {
|
|
3539
3659
|
if (!raw || typeof raw !== "object") continue;
|
|
3540
3660
|
const tools = Array.isArray(raw.tools)
|
|
3541
3661
|
? raw.tools.filter((t) => typeof t === "string")
|
|
@@ -3548,7 +3668,6 @@ function parseManifestPermissions(packageJson) {
|
|
|
3548
3668
|
: [];
|
|
3549
3669
|
servers[serverName] = { tools, readPaths, writePaths };
|
|
3550
3670
|
}
|
|
3551
|
-
|
|
3552
3671
|
return { servers };
|
|
3553
3672
|
}
|
|
3554
3673
|
|
|
@@ -3591,15 +3710,36 @@ function resolveWidgetPackagePath(widgetId) {
|
|
|
3591
3710
|
return path$f.join(widgetsRoot, ...parts, "package.json");
|
|
3592
3711
|
}
|
|
3593
3712
|
|
|
3713
|
+
/**
|
|
3714
|
+
* Pull the bare component name (last dot-segment) from a dotted
|
|
3715
|
+
* widget id. Returns null for npm-form ids (`@scope/pkg`) or 1-segment
|
|
3716
|
+
* ids — those have no per-widget component to scope by.
|
|
3717
|
+
*/
|
|
3718
|
+
function _componentNameFromWidgetId(widgetId) {
|
|
3719
|
+
if (typeof widgetId !== "string" || !widgetId) return null;
|
|
3720
|
+
if (widgetId.startsWith("@")) return null;
|
|
3721
|
+
const parts = widgetId.split(".");
|
|
3722
|
+
if (parts.length < 3) return null;
|
|
3723
|
+
return parts[parts.length - 1] || null;
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3594
3726
|
/**
|
|
3595
3727
|
* Read and parse a widget's MCP permissions. Returns null if:
|
|
3596
3728
|
* - the widget directory doesn't exist
|
|
3597
3729
|
* - package.json is unreadable / malformed
|
|
3598
3730
|
* - the widget hasn't declared dash.permissions.mcp
|
|
3599
3731
|
*
|
|
3600
|
-
*
|
|
3732
|
+
* For 3-part scoped widget ids (`scope.pkg.component`), prefers the
|
|
3733
|
+
* package's `dash.permissions.mcpByComponent[component]` block when
|
|
3734
|
+
* present. Falls back to the package-level `mcp` block when the
|
|
3735
|
+
* per-component breakdown isn't available — preserves the previous
|
|
3736
|
+
* behavior for older packages that haven't been re-scanned.
|
|
3737
|
+
*
|
|
3738
|
+
* Result is cached per widgetId for the lifetime of this process —
|
|
3739
|
+
* each component gets its own cache entry, so a 10-widget package
|
|
3740
|
+
* has 10 entries, each with its own server/tools subset.
|
|
3601
3741
|
*/
|
|
3602
|
-
function getWidgetMcpPermissions$
|
|
3742
|
+
function getWidgetMcpPermissions$2(widgetId) {
|
|
3603
3743
|
if (_cache.has(widgetId)) return _cache.get(widgetId);
|
|
3604
3744
|
const pkgPath = resolveWidgetPackagePath(widgetId);
|
|
3605
3745
|
if (!pkgPath || !fs$c.existsSync(pkgPath)) {
|
|
@@ -3609,7 +3749,8 @@ function getWidgetMcpPermissions$1(widgetId) {
|
|
|
3609
3749
|
try {
|
|
3610
3750
|
const raw = fs$c.readFileSync(pkgPath, "utf8");
|
|
3611
3751
|
const pkg = JSON.parse(raw);
|
|
3612
|
-
const
|
|
3752
|
+
const componentName = _componentNameFromWidgetId(widgetId);
|
|
3753
|
+
const perms = parseManifestPermissions(pkg, componentName);
|
|
3613
3754
|
_cache.set(widgetId, perms);
|
|
3614
3755
|
return perms;
|
|
3615
3756
|
} catch (e) {
|
|
@@ -3629,7 +3770,7 @@ function clearCache() {
|
|
|
3629
3770
|
}
|
|
3630
3771
|
|
|
3631
3772
|
var widgetPermissions = {
|
|
3632
|
-
getWidgetMcpPermissions: getWidgetMcpPermissions$
|
|
3773
|
+
getWidgetMcpPermissions: getWidgetMcpPermissions$2,
|
|
3633
3774
|
parseManifestPermissions,
|
|
3634
3775
|
dottedComponentIdToPackageId,
|
|
3635
3776
|
expandHome,
|
|
@@ -4037,15 +4178,85 @@ function requireScanWidgetPackagePermissions () {
|
|
|
4037
4178
|
return out;
|
|
4038
4179
|
}
|
|
4039
4180
|
|
|
4181
|
+
/**
|
|
4182
|
+
* Locate the package's `widgets/` directory and enumerate every
|
|
4183
|
+
* `<Name>.js` paired with a `<Name>.dash.js` config. That pair is the
|
|
4184
|
+
* canonical widget shape across the repo (see
|
|
4185
|
+
* `widgetCompiler.findWidgetsDir`). Returns
|
|
4186
|
+
* [{componentName, componentFilePath}].
|
|
4187
|
+
*
|
|
4188
|
+
* Files without a sibling `.dash.js` (utils, helpers, contexts) are
|
|
4189
|
+
* skipped — they're not widgets, so attributing their MCP usage to a
|
|
4190
|
+
* specific component is ambiguous.
|
|
4191
|
+
*/
|
|
4192
|
+
function _findComponentFiles(packageDir) {
|
|
4193
|
+
const candidates = [
|
|
4194
|
+
path.join(packageDir, "widgets"),
|
|
4195
|
+
path.join(packageDir, "src", "widgets"),
|
|
4196
|
+
];
|
|
4197
|
+
const out = [];
|
|
4198
|
+
for (const widgetsDir of candidates) {
|
|
4199
|
+
if (!fs.existsSync(widgetsDir)) continue;
|
|
4200
|
+
let entries;
|
|
4201
|
+
try {
|
|
4202
|
+
entries = fs.readdirSync(widgetsDir, { withFileTypes: true });
|
|
4203
|
+
} catch (_) {
|
|
4204
|
+
continue;
|
|
4205
|
+
}
|
|
4206
|
+
for (const entry of entries) {
|
|
4207
|
+
if (!entry.isFile()) continue;
|
|
4208
|
+
const m = entry.name.match(/^(.+)\.dash\.(?:js|jsx|ts|tsx)$/);
|
|
4209
|
+
if (!m) continue;
|
|
4210
|
+
const componentName = m[1];
|
|
4211
|
+
// Prefer .js, then .jsx, .ts, .tsx — first-match wins.
|
|
4212
|
+
for (const ext of [".js", ".jsx", ".ts", ".tsx"]) {
|
|
4213
|
+
const componentFilePath = path.join(
|
|
4214
|
+
widgetsDir,
|
|
4215
|
+
`${componentName}${ext}`,
|
|
4216
|
+
);
|
|
4217
|
+
if (fs.existsSync(componentFilePath)) {
|
|
4218
|
+
out.push({ componentName, componentFilePath });
|
|
4219
|
+
break;
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
}
|
|
4224
|
+
return out;
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4040
4227
|
/**
|
|
4041
4228
|
* Walk packageDir, scan every source file, return the canonical
|
|
4042
4229
|
* `dash.permissions.mcp` block. Returns `{}` when no MCP usage found.
|
|
4230
|
+
*
|
|
4231
|
+
* Per-file pairing — providers and tools are paired at the source-file
|
|
4232
|
+
* level, not unioned across the whole package. The vast majority of
|
|
4233
|
+
* widget source files use a single `useMcpProvider("type")` hook and
|
|
4234
|
+
* call its tools in the same file; pairing per file produces the
|
|
4235
|
+
* correct provider→tools mapping for that case.
|
|
4236
|
+
*
|
|
4237
|
+
* Multi-widget packages (one package shipping N widgets, each with a
|
|
4238
|
+
* different provider) used to be miscompiled by the old whole-package
|
|
4239
|
+
* union: every provider in the package got credit for every tool any
|
|
4240
|
+
* sibling widget called. The result was a manifest where, e.g., the
|
|
4241
|
+
* `slack` server claimed `list_pull_requests` and `list_directory`
|
|
4242
|
+
* tools simply because the same package also shipped a GitHub widget
|
|
4243
|
+
* and a Filesystem widget. Per-file pairing eliminates that
|
|
4244
|
+
* cross-pollination because each file's tools attach only to that
|
|
4245
|
+
* file's providers.
|
|
4246
|
+
*
|
|
4247
|
+
* Files containing multiple providers (a widget that orchestrates two
|
|
4248
|
+
* MCP servers in the same component) attribute their tools to each
|
|
4249
|
+
* provider in that file — an acceptable over-grant within the single
|
|
4250
|
+
* widget's own scope, and a rare-enough case that we don't try to
|
|
4251
|
+
* parse expression scoping. Tools called in files with NO provider
|
|
4252
|
+
* hook (e.g. a helper module called from a parent widget) are ignored
|
|
4253
|
+
* — a runtime hint without static evidence of which provider it
|
|
4254
|
+
* targets falls through to the gate's JIT consent.
|
|
4043
4255
|
*/
|
|
4044
4256
|
function scanWidgetPackagePermissions(packageDir) {
|
|
4045
4257
|
if (typeof packageDir !== "string" || !fs.existsSync(packageDir)) return {};
|
|
4046
4258
|
|
|
4047
|
-
const
|
|
4048
|
-
const allTools = new Set();
|
|
4259
|
+
const providerToTools = new Map();
|
|
4049
4260
|
for (const filePath of _walkSourceFiles(packageDir)) {
|
|
4050
4261
|
let code;
|
|
4051
4262
|
try {
|
|
@@ -4054,15 +4265,81 @@ function requireScanWidgetPackagePermissions () {
|
|
|
4054
4265
|
continue;
|
|
4055
4266
|
}
|
|
4056
4267
|
const { providers, tools } = scanFileForMcpUsage(code);
|
|
4057
|
-
|
|
4058
|
-
|
|
4268
|
+
// No provider in this file → tools have no statically-known
|
|
4269
|
+
// server to attach to. Skip rather than cross-pollinate.
|
|
4270
|
+
if (providers.length === 0) continue;
|
|
4271
|
+
// No tools in this file → still register the provider with an
|
|
4272
|
+
// empty set so the manifest knows the widget declares this
|
|
4273
|
+
// server. (Edge case: helper file that mounts useMcpProvider but
|
|
4274
|
+
// calls callTool via an indirection we can't statically read.)
|
|
4275
|
+
for (const p of providers) {
|
|
4276
|
+
if (!providerToTools.has(p)) providerToTools.set(p, new Set());
|
|
4277
|
+
const bucket = providerToTools.get(p);
|
|
4278
|
+
for (const t of tools) bucket.add(t);
|
|
4279
|
+
}
|
|
4059
4280
|
}
|
|
4060
4281
|
|
|
4061
|
-
|
|
4282
|
+
// Drop providers that ended up with no tools across the whole
|
|
4283
|
+
// package — declaring a server with an empty tool list is
|
|
4284
|
+
// misleading downstream (the Permissions panel renders an empty
|
|
4285
|
+
// section, the gate treats it as "all tools require JIT"). The
|
|
4286
|
+
// runtime gate is the safety net for tools we couldn't statically
|
|
4287
|
+
// pair.
|
|
4288
|
+
const out = {};
|
|
4289
|
+
for (const [provider, toolSet] of providerToTools.entries()) {
|
|
4290
|
+
if (toolSet.size === 0) continue;
|
|
4291
|
+
out[provider] = { tools: Array.from(toolSet).sort() };
|
|
4292
|
+
}
|
|
4293
|
+
return out;
|
|
4294
|
+
}
|
|
4062
4295
|
|
|
4296
|
+
/**
|
|
4297
|
+
* Per-component scanner. Walks each `widgets/<Name>.js` paired with
|
|
4298
|
+
* `widgets/<Name>.dash.js` and produces a map
|
|
4299
|
+
* { [componentName]: { [server]: { tools: [...] } } }
|
|
4300
|
+
*
|
|
4301
|
+
* Each component's entry contains ONLY the providers + tools its own
|
|
4302
|
+
* file uses. Sibling widgets in the same package don't bleed into
|
|
4303
|
+
* each other — the bug the package-level `mcp` block carries by
|
|
4304
|
+
* design (one shared block for the whole package) is closed here at
|
|
4305
|
+
* the widget granularity.
|
|
4306
|
+
*
|
|
4307
|
+
* Used by `widgetPermissions.getWidgetMcpPermissions(scopedWidgetId)`
|
|
4308
|
+
* to answer "what does THIS specific widget declare" instead of
|
|
4309
|
+
* "what does the WHOLE package declare", which is what fixes:
|
|
4310
|
+
* - Permissions panel showing every sibling's declarations under
|
|
4311
|
+
* every widget
|
|
4312
|
+
* - JIT consent's sibling-fanout writing grants to widgets that
|
|
4313
|
+
* don't actually use the granted tool
|
|
4314
|
+
*
|
|
4315
|
+
* Returns `{}` when no component file declares any MCP usage.
|
|
4316
|
+
*/
|
|
4317
|
+
function scanWidgetPackagePermissionsByComponent(packageDir) {
|
|
4318
|
+
if (typeof packageDir !== "string" || !fs.existsSync(packageDir)) return {};
|
|
4063
4319
|
const out = {};
|
|
4064
|
-
for (const
|
|
4065
|
-
|
|
4320
|
+
for (const { componentName, componentFilePath } of _findComponentFiles(
|
|
4321
|
+
packageDir,
|
|
4322
|
+
)) {
|
|
4323
|
+
let code;
|
|
4324
|
+
try {
|
|
4325
|
+
code = fs.readFileSync(componentFilePath, "utf8");
|
|
4326
|
+
} catch (_) {
|
|
4327
|
+
continue;
|
|
4328
|
+
}
|
|
4329
|
+
const { providers, tools } = scanFileForMcpUsage(code);
|
|
4330
|
+
if (providers.length === 0) continue;
|
|
4331
|
+
// Pair this file's providers with this file's tools — same
|
|
4332
|
+
// per-file logic as `scanWidgetPackagePermissions`, scoped to
|
|
4333
|
+
// a single widget's component file.
|
|
4334
|
+
const servers = {};
|
|
4335
|
+
for (const provider of providers) {
|
|
4336
|
+
const sorted = Array.from(new Set(tools)).sort();
|
|
4337
|
+
if (sorted.length === 0) continue;
|
|
4338
|
+
servers[provider] = { tools: sorted };
|
|
4339
|
+
}
|
|
4340
|
+
if (Object.keys(servers).length > 0) {
|
|
4341
|
+
out[componentName] = { servers };
|
|
4342
|
+
}
|
|
4066
4343
|
}
|
|
4067
4344
|
return out;
|
|
4068
4345
|
}
|
|
@@ -4099,8 +4376,21 @@ function requireScanWidgetPackagePermissions () {
|
|
|
4099
4376
|
|
|
4100
4377
|
/**
|
|
4101
4378
|
* Apply the scanner's findings to `package.json` in `packageDir` —
|
|
4102
|
-
* read, merge with any existing
|
|
4103
|
-
*
|
|
4379
|
+
* read, merge with any existing manifest, write back. Returns the
|
|
4380
|
+
* resulting package-level `mcp` block (or null if no package.json AND
|
|
4381
|
+
* no usage to write).
|
|
4382
|
+
*
|
|
4383
|
+
* Writes BOTH blocks:
|
|
4384
|
+
* - `dash.permissions.mcp` — package-level union (back-compat with
|
|
4385
|
+
* the gate's pre-per-component lookup path)
|
|
4386
|
+
* - `dash.permissions.mcpByComponent` — per-widget breakdown so the
|
|
4387
|
+
* Permissions panel and JIT-consent sibling fanout can answer
|
|
4388
|
+
* "which specific widgets declare this tool" without cross-
|
|
4389
|
+
* contaminating siblings
|
|
4390
|
+
*
|
|
4391
|
+
* The per-component block is fully scanner-derived (no hand-merge) —
|
|
4392
|
+
* editing it by hand is unsupported. Package-level `mcp` continues
|
|
4393
|
+
* to honor hand-authored entries.
|
|
4104
4394
|
*/
|
|
4105
4395
|
function applyScanToPackageJson(packageDir) {
|
|
4106
4396
|
const pkgPath = path.join(packageDir, "package.json");
|
|
@@ -4115,18 +4405,42 @@ function requireScanWidgetPackagePermissions () {
|
|
|
4115
4405
|
const scanned = scanWidgetPackagePermissions(packageDir);
|
|
4116
4406
|
const human = existing.dash?.permissions?.mcp || null;
|
|
4117
4407
|
const merged = mergePermissions(human, scanned);
|
|
4118
|
-
|
|
4408
|
+
const byComponent = scanWidgetPackagePermissionsByComponent(packageDir);
|
|
4409
|
+
// Existing stale `mcpByComponent` from a prior scan must be dropped
|
|
4410
|
+
// when this run finds no qualifying components — otherwise leftover
|
|
4411
|
+
// entries for deleted widgets keep applying permissions the user
|
|
4412
|
+
// didn't actually re-approve. Treat "has existing block but new
|
|
4413
|
+
// scan empty" as a write trigger (with the side effect of removing
|
|
4414
|
+
// the block via the delete below).
|
|
4415
|
+
const hadByComponent =
|
|
4416
|
+
!!existing.dash?.permissions?.mcpByComponent &&
|
|
4417
|
+
typeof existing.dash.permissions.mcpByComponent === "object" &&
|
|
4418
|
+
Object.keys(existing.dash.permissions.mcpByComponent).length > 0;
|
|
4419
|
+
if (
|
|
4420
|
+
Object.keys(merged).length === 0 &&
|
|
4421
|
+
Object.keys(byComponent).length === 0 &&
|
|
4422
|
+
!hadByComponent
|
|
4423
|
+
) {
|
|
4119
4424
|
// Nothing to write. Don't churn the file.
|
|
4120
4425
|
return null;
|
|
4121
4426
|
}
|
|
4427
|
+
const nextPermissions = {
|
|
4428
|
+
...((existing.dash || {}).permissions || {}),
|
|
4429
|
+
};
|
|
4430
|
+
if (Object.keys(merged).length > 0) nextPermissions.mcp = merged;
|
|
4431
|
+
if (Object.keys(byComponent).length > 0) {
|
|
4432
|
+
nextPermissions.mcpByComponent = byComponent;
|
|
4433
|
+
} else {
|
|
4434
|
+
// Drop a stale `mcpByComponent` block when re-scanning finds no
|
|
4435
|
+
// qualifying component files. Prevents leftover entries from a
|
|
4436
|
+
// prior install lingering after the user deletes a widget.
|
|
4437
|
+
delete nextPermissions.mcpByComponent;
|
|
4438
|
+
}
|
|
4122
4439
|
const next = {
|
|
4123
4440
|
...existing,
|
|
4124
4441
|
dash: {
|
|
4125
4442
|
...(existing.dash || {}),
|
|
4126
|
-
permissions:
|
|
4127
|
-
...((existing.dash || {}).permissions || {}),
|
|
4128
|
-
mcp: merged,
|
|
4129
|
-
},
|
|
4443
|
+
permissions: nextPermissions,
|
|
4130
4444
|
},
|
|
4131
4445
|
};
|
|
4132
4446
|
fs.writeFileSync(pkgPath, JSON.stringify(next, null, 2), "utf8");
|
|
@@ -4163,6 +4477,7 @@ function requireScanWidgetPackagePermissions () {
|
|
|
4163
4477
|
scanWidgetPackagePermissions_1 = {
|
|
4164
4478
|
scanFileForMcpUsage,
|
|
4165
4479
|
scanWidgetPackagePermissions,
|
|
4480
|
+
scanWidgetPackagePermissionsByComponent,
|
|
4166
4481
|
mergePermissions,
|
|
4167
4482
|
applyScanToPackageJson,
|
|
4168
4483
|
backfillPackagePermissions,
|
|
@@ -26546,6 +26861,7 @@ const { safePath: safePath$1 } = safePath_1;
|
|
|
26546
26861
|
const { requestApproval } = jitConsent$1;
|
|
26547
26862
|
const { lookup: lookupMountToken } = mountTokenRegistry;
|
|
26548
26863
|
const { resolveSiblings } = resolveSiblings_1;
|
|
26864
|
+
const { getWidgetMcpPermissions: getWidgetMcpPermissions$1 } = widgetPermissions;
|
|
26549
26865
|
|
|
26550
26866
|
// Lazy default for the registry snapshot — `widgetRegistry.js` pulls
|
|
26551
26867
|
// in a lot, so we don't want to require it at module-load time. The
|
|
@@ -26696,6 +27012,66 @@ function gateToolCall$1({ widgetId, token, serverName, toolName, args }) {
|
|
|
26696
27012
|
return { allow: true };
|
|
26697
27013
|
}
|
|
26698
27014
|
|
|
27015
|
+
/**
|
|
27016
|
+
* Filter a sibling-widget list down to widgets whose static manifest
|
|
27017
|
+
* declares the given `serverName + toolName`. The originating widget
|
|
27018
|
+
* (`originatingWidgetId`) is always retained — the call IS proof of
|
|
27019
|
+
* use even if the manifest hasn't been re-scanned to declare the tool
|
|
27020
|
+
* (e.g., variable-indirect `callTool(name, …)` calls that the scanner
|
|
27021
|
+
* intentionally skips).
|
|
27022
|
+
*
|
|
27023
|
+
* Back-compat: if no sibling has any per-widget declaration at all
|
|
27024
|
+
* (package never re-scanned with the per-component scanner), the
|
|
27025
|
+
* filter returns the unmodified list — the user's pre-per-component
|
|
27026
|
+
* "Apply to all in package" behavior is preserved for those packages.
|
|
27027
|
+
*
|
|
27028
|
+
* Used by the JIT consent path to:
|
|
27029
|
+
* - Set the modal's "Apply to all N widgets" count to the actually-
|
|
27030
|
+
* applicable subset
|
|
27031
|
+
* - Limit the grant fanout on approval to the same subset
|
|
27032
|
+
*
|
|
27033
|
+
* Pure helper modulo `getWidgetMcpPermissions` (which is cached + fs
|
|
27034
|
+
* memo-ish per process).
|
|
27035
|
+
*/
|
|
27036
|
+
function _filterSiblingsByDeclaration(
|
|
27037
|
+
siblingWidgetIds,
|
|
27038
|
+
originatingWidgetId,
|
|
27039
|
+
serverName,
|
|
27040
|
+
toolName,
|
|
27041
|
+
) {
|
|
27042
|
+
if (!Array.isArray(siblingWidgetIds) || siblingWidgetIds.length === 0) {
|
|
27043
|
+
return [originatingWidgetId];
|
|
27044
|
+
}
|
|
27045
|
+
// Look up each sibling's per-widget manifest. We need to know
|
|
27046
|
+
// whether ANY sibling has the per-component breakdown — if not,
|
|
27047
|
+
// we're in back-compat territory and should not filter.
|
|
27048
|
+
const lookups = siblingWidgetIds.map((sibId) => ({
|
|
27049
|
+
sibId,
|
|
27050
|
+
perms: getWidgetMcpPermissions$1(sibId),
|
|
27051
|
+
}));
|
|
27052
|
+
const anyManifested = lookups.some(
|
|
27053
|
+
(l) =>
|
|
27054
|
+
l.perms && l.perms.servers && Object.keys(l.perms.servers).length > 0,
|
|
27055
|
+
);
|
|
27056
|
+
if (!anyManifested) return siblingWidgetIds;
|
|
27057
|
+
|
|
27058
|
+
const out = [];
|
|
27059
|
+
for (const { sibId, perms } of lookups) {
|
|
27060
|
+
if (sibId === originatingWidgetId) {
|
|
27061
|
+
out.push(sibId);
|
|
27062
|
+
continue;
|
|
27063
|
+
}
|
|
27064
|
+
const tools = perms?.servers?.[serverName]?.tools;
|
|
27065
|
+
if (Array.isArray(tools) && tools.includes(toolName)) {
|
|
27066
|
+
out.push(sibId);
|
|
27067
|
+
}
|
|
27068
|
+
}
|
|
27069
|
+
// Defensive: if filtering somehow eliminated everything (shouldn't
|
|
27070
|
+
// happen since the originating widget is always retained), fall
|
|
27071
|
+
// back to the single originating widget.
|
|
27072
|
+
return out.length > 0 ? out : [originatingWidgetId];
|
|
27073
|
+
}
|
|
27074
|
+
|
|
26699
27075
|
/**
|
|
26700
27076
|
* Merge `addition` (a grant blob) into the widget's existing grant. Used
|
|
26701
27077
|
* by the JIT path to extend an existing grant with a new tool/path
|
|
@@ -26787,6 +27163,25 @@ async function gateToolCallWithJit$1(req, opts = {}) {
|
|
|
26787
27163
|
opts.getRegistrySnapshot || _defaultRegistrySnapshot;
|
|
26788
27164
|
const siblingInfo = resolveSiblings(verifiedWidgetId, getRegistrySnapshot());
|
|
26789
27165
|
|
|
27166
|
+
// Filter siblings to those whose per-component manifest actually
|
|
27167
|
+
// declares the requested `serverName + toolName`. Without this
|
|
27168
|
+
// filter, "Apply to all in package" writes the grant to every
|
|
27169
|
+
// sibling regardless of whether that sibling uses the tool — a
|
|
27170
|
+
// Counter widget ends up with Google Drive grants it doesn't
|
|
27171
|
+
// need. The originating widget is always included (its runtime
|
|
27172
|
+
// call IS proof) even if its manifest hasn't been re-scanned to
|
|
27173
|
+
// declare the tool.
|
|
27174
|
+
//
|
|
27175
|
+
// Falls back to the unfiltered sibling list when no per-component
|
|
27176
|
+
// data exists on ANY sibling (back-compat for older packages that
|
|
27177
|
+
// haven't been re-scanned with the new scanner).
|
|
27178
|
+
const applicableSiblingIds = _filterSiblingsByDeclaration(
|
|
27179
|
+
siblingInfo.siblingWidgetIds,
|
|
27180
|
+
verifiedWidgetId,
|
|
27181
|
+
req.serverName,
|
|
27182
|
+
req.toolName,
|
|
27183
|
+
);
|
|
27184
|
+
|
|
26790
27185
|
let decision;
|
|
26791
27186
|
try {
|
|
26792
27187
|
decision = await requestApproval(
|
|
@@ -26800,7 +27195,7 @@ async function gateToolCallWithJit$1(req, opts = {}) {
|
|
|
26800
27195
|
args: req.args || {},
|
|
26801
27196
|
},
|
|
26802
27197
|
packageId: siblingInfo.packageId,
|
|
26803
|
-
siblingWidgetIds:
|
|
27198
|
+
siblingWidgetIds: applicableSiblingIds,
|
|
26804
27199
|
},
|
|
26805
27200
|
{ timeoutMs: opts.timeoutMs },
|
|
26806
27201
|
);
|
|
@@ -26851,11 +27246,17 @@ async function gateToolCallWithJit$1(req, opts = {}) {
|
|
|
26851
27246
|
// its prior grants on other servers/tools. When the option is off
|
|
26852
27247
|
// (or no siblings were resolved), behave as before: single-widget
|
|
26853
27248
|
// write to the requesting widget only.
|
|
27249
|
+
//
|
|
27250
|
+
// Use the FILTERED sibling list (`applicableSiblingIds`) here — the
|
|
27251
|
+
// grant only fans out to widgets whose manifest actually declares
|
|
27252
|
+
// the server+tool. The modal showed the same filtered count, so
|
|
27253
|
+
// the user's "Apply to all N widgets" decision was already made
|
|
27254
|
+
// against the correct set.
|
|
26854
27255
|
const targetWidgetIds =
|
|
26855
27256
|
decision.applyToSiblings === true &&
|
|
26856
|
-
Array.isArray(
|
|
26857
|
-
|
|
26858
|
-
?
|
|
27257
|
+
Array.isArray(applicableSiblingIds) &&
|
|
27258
|
+
applicableSiblingIds.length > 1
|
|
27259
|
+
? applicableSiblingIds
|
|
26859
27260
|
: [verifiedWidgetId];
|
|
26860
27261
|
|
|
26861
27262
|
try {
|