@trops/dash-core 0.1.496 → 0.1.498
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 +451 -36
- package/dist/electron/index.js.map +1 -1
- package/dist/index.esm.js +578 -391
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +578 -391
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/electron/index.js
CHANGED
|
@@ -17,7 +17,7 @@ var require$$0$5 = require('@modelcontextprotocol/sdk/client/index.js');
|
|
|
17
17
|
var require$$1$4 = require('@modelcontextprotocol/sdk/client/stdio.js');
|
|
18
18
|
var require$$0$4 = require('pkce-challenge');
|
|
19
19
|
var require$$2$1 = require('os');
|
|
20
|
-
var require$$
|
|
20
|
+
var require$$12 = require('child_process');
|
|
21
21
|
var require$$3$2 = require('adm-zip');
|
|
22
22
|
var require$$4$1 = require('url');
|
|
23
23
|
var require$$2$2 = require('vm');
|
|
@@ -1873,7 +1873,7 @@ var safePath_1 = {
|
|
|
1873
1873
|
// it can mark the relative import as transitive-external and then fail
|
|
1874
1874
|
// to resolve back. Deferring the require breaks the static-analysis
|
|
1875
1875
|
// chain so the rollup build resolves cleanly.
|
|
1876
|
-
const DEFAULT_TIMEOUT_MS = 1000;
|
|
1876
|
+
const DEFAULT_TIMEOUT_MS$1 = 1000;
|
|
1877
1877
|
const DEFAULT_MEMORY_BYTES = 32 * 1024 * 1024; // 32 MB
|
|
1878
1878
|
|
|
1879
1879
|
let _modulePromise = null;
|
|
@@ -1936,7 +1936,7 @@ async function runOnce({
|
|
|
1936
1936
|
body,
|
|
1937
1937
|
args = [],
|
|
1938
1938
|
inputs = [],
|
|
1939
|
-
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
1939
|
+
timeoutMs = DEFAULT_TIMEOUT_MS$1,
|
|
1940
1940
|
memoryBytes = DEFAULT_MEMORY_BYTES,
|
|
1941
1941
|
}) {
|
|
1942
1942
|
if (typeof body !== "string" || !body.trim()) {
|
|
@@ -2007,7 +2007,7 @@ async function createCompiled({
|
|
|
2007
2007
|
let disposed = false;
|
|
2008
2008
|
|
|
2009
2009
|
return {
|
|
2010
|
-
run(inputs, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
2010
|
+
run(inputs, timeoutMs = DEFAULT_TIMEOUT_MS$1) {
|
|
2011
2011
|
if (disposed) {
|
|
2012
2012
|
return { error: "executor already disposed" };
|
|
2013
2013
|
}
|
|
@@ -2048,7 +2048,7 @@ async function createCompiled({
|
|
|
2048
2048
|
var safeJsExecutor$1 = {
|
|
2049
2049
|
runOnce,
|
|
2050
2050
|
createCompiled,
|
|
2051
|
-
DEFAULT_TIMEOUT_MS,
|
|
2051
|
+
DEFAULT_TIMEOUT_MS: DEFAULT_TIMEOUT_MS$1,
|
|
2052
2052
|
DEFAULT_MEMORY_BYTES,
|
|
2053
2053
|
};
|
|
2054
2054
|
|
|
@@ -21235,13 +21235,23 @@ function writeToDisk(data) {
|
|
|
21235
21235
|
fs$b.renameSync(tmp, p);
|
|
21236
21236
|
}
|
|
21237
21237
|
|
|
21238
|
-
// Recognized origins for a persisted grant.
|
|
21239
|
-
// approved against the developer's declared
|
|
21240
|
-
//
|
|
21241
|
-
//
|
|
21242
|
-
//
|
|
21238
|
+
// Recognized origins for a persisted grant.
|
|
21239
|
+
// "declared" — user approved against the developer's declared
|
|
21240
|
+
// dash.permissions.mcp block at install time.
|
|
21241
|
+
// "discovered" — install-time scanner produced a synthetic manifest
|
|
21242
|
+
// the user approved.
|
|
21243
|
+
// "manual" — user typed entries themselves in
|
|
21244
|
+
// Settings → Privacy & Security with no manifest backing.
|
|
21245
|
+
// "live" — user approved a just-in-time consent prompt at
|
|
21246
|
+
// runtime when a tool call hit the gate without a
|
|
21247
|
+
// matching grant.
|
|
21243
21248
|
// Other values are dropped on persist (legacy grants stay null).
|
|
21244
|
-
const ALLOWED_GRANT_ORIGINS = new Set([
|
|
21249
|
+
const ALLOWED_GRANT_ORIGINS = new Set([
|
|
21250
|
+
"declared",
|
|
21251
|
+
"discovered",
|
|
21252
|
+
"manual",
|
|
21253
|
+
"live",
|
|
21254
|
+
]);
|
|
21245
21255
|
|
|
21246
21256
|
/**
|
|
21247
21257
|
* Sanitize a perms object before persisting. Drops unknown keys, coerces
|
|
@@ -21289,7 +21299,7 @@ function getGrant$2(widgetId) {
|
|
|
21289
21299
|
return all[widgetId] || null;
|
|
21290
21300
|
}
|
|
21291
21301
|
|
|
21292
|
-
function setGrant$
|
|
21302
|
+
function setGrant$2(widgetId, perms) {
|
|
21293
21303
|
if (typeof widgetId !== "string" || !widgetId) return false;
|
|
21294
21304
|
const sanitized = sanitizePerms(perms);
|
|
21295
21305
|
if (!sanitized) return false;
|
|
@@ -21371,7 +21381,7 @@ function clearCache$1() {
|
|
|
21371
21381
|
|
|
21372
21382
|
var grantedPermissions = {
|
|
21373
21383
|
getGrant: getGrant$2,
|
|
21374
|
-
setGrant: setGrant$
|
|
21384
|
+
setGrant: setGrant$2,
|
|
21375
21385
|
revokeGrant: revokeGrant$1,
|
|
21376
21386
|
revokeServer: revokeServer$1,
|
|
21377
21387
|
listAllGrants: listAllGrants$1,
|
|
@@ -21379,6 +21389,218 @@ var grantedPermissions = {
|
|
|
21379
21389
|
ALLOWED_GRANT_ORIGINS,
|
|
21380
21390
|
};
|
|
21381
21391
|
|
|
21392
|
+
/**
|
|
21393
|
+
* jitConsent.js
|
|
21394
|
+
*
|
|
21395
|
+
* Just-in-time permission consent for widget→backend calls.
|
|
21396
|
+
*
|
|
21397
|
+
* When a widget hits a gate without an existing grant for the requested
|
|
21398
|
+
* (domain, action, args), the gate calls `requestApproval` which:
|
|
21399
|
+
* 1. Synchronously emits `widget:permission-required` to all
|
|
21400
|
+
* BrowserWindows with a unique requestId.
|
|
21401
|
+
* 2. Returns a Promise that resolves on user response or rejects on
|
|
21402
|
+
* timeout.
|
|
21403
|
+
* 3. Coalesces requests with the same coalescing key so a widget
|
|
21404
|
+
* bursting identical calls produces one prompt, not many.
|
|
21405
|
+
*
|
|
21406
|
+
* The renderer's JitConsentModal subscribes to the event, presents the
|
|
21407
|
+
* user with granularity options (this once / this tool / this tool +
|
|
21408
|
+
* parent dir), and replies via `widget:permission-response` with
|
|
21409
|
+
* `{ requestId, decision }`. main.js wires the IPC handler back to
|
|
21410
|
+
* `_handleResponse`.
|
|
21411
|
+
*
|
|
21412
|
+
* The module is intentionally domain-agnostic in shape — the request
|
|
21413
|
+
* payload carries `domain` so future plug-ins (fs, algolia, llm) reuse
|
|
21414
|
+
* the same machinery. Phase 1 only emits with `domain: "mcp"`.
|
|
21415
|
+
*
|
|
21416
|
+
* Public surface:
|
|
21417
|
+
* requestApproval(req, opts) → Promise<{ approve, scope?, ... }>
|
|
21418
|
+
* _handleResponse({ requestId, decision }) → void (called from main.js IPC)
|
|
21419
|
+
* _resetForTest() → void (test-only)
|
|
21420
|
+
*/
|
|
21421
|
+
|
|
21422
|
+
const { BrowserWindow: BrowserWindow$2, ipcMain: ipcMain$2 } = require$$0$1;
|
|
21423
|
+
|
|
21424
|
+
const REQUEST_CHANNEL = "widget:permission-required";
|
|
21425
|
+
const RESPONSE_CHANNEL = "widget:permission-response";
|
|
21426
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
21427
|
+
|
|
21428
|
+
// requestId → { resolve, reject, timeout, coalesceKey, joinedResolvers }
|
|
21429
|
+
const _pending = new Map();
|
|
21430
|
+
// coalesceKey → requestId (so duplicate requests join the live one)
|
|
21431
|
+
const _coalesce = new Map();
|
|
21432
|
+
let _idCounter = 0;
|
|
21433
|
+
|
|
21434
|
+
function nextRequestId() {
|
|
21435
|
+
_idCounter += 1;
|
|
21436
|
+
return `jit-${Date.now()}-${_idCounter}`;
|
|
21437
|
+
}
|
|
21438
|
+
|
|
21439
|
+
/**
|
|
21440
|
+
* Build a coalescing key from the request. Two requests share the same
|
|
21441
|
+
* key iff they're "the same prompt" — same widget, same domain+action,
|
|
21442
|
+
* same target server/tool. Args beyond that (e.g. exact path) DON'T
|
|
21443
|
+
* differentiate; if the user is being asked about read_file already,
|
|
21444
|
+
* approving handles all current paths.
|
|
21445
|
+
*/
|
|
21446
|
+
function coalesceKeyOf(req) {
|
|
21447
|
+
if (req.domain === "mcp") {
|
|
21448
|
+
const innerArgs = req.args || {};
|
|
21449
|
+
return [
|
|
21450
|
+
req.widgetId,
|
|
21451
|
+
"mcp",
|
|
21452
|
+
innerArgs.serverName || "",
|
|
21453
|
+
innerArgs.toolName || "",
|
|
21454
|
+
].join("::");
|
|
21455
|
+
}
|
|
21456
|
+
// Default: domain + action + serialized top-level args
|
|
21457
|
+
return [
|
|
21458
|
+
req.widgetId,
|
|
21459
|
+
req.domain,
|
|
21460
|
+
req.action,
|
|
21461
|
+
JSON.stringify(req.args || {}),
|
|
21462
|
+
].join("::");
|
|
21463
|
+
}
|
|
21464
|
+
|
|
21465
|
+
function emitEvent(payload) {
|
|
21466
|
+
let wins = [];
|
|
21467
|
+
try {
|
|
21468
|
+
wins = BrowserWindow$2.getAllWindows() || [];
|
|
21469
|
+
} catch {
|
|
21470
|
+
wins = [];
|
|
21471
|
+
}
|
|
21472
|
+
for (const w of wins) {
|
|
21473
|
+
try {
|
|
21474
|
+
w?.webContents?.send?.(REQUEST_CHANNEL, payload);
|
|
21475
|
+
} catch {
|
|
21476
|
+
// best-effort broadcast
|
|
21477
|
+
}
|
|
21478
|
+
}
|
|
21479
|
+
}
|
|
21480
|
+
|
|
21481
|
+
function validateRequest(req) {
|
|
21482
|
+
if (!req || typeof req !== "object") return "invalid request: not an object";
|
|
21483
|
+
if (typeof req.widgetId !== "string" || !req.widgetId)
|
|
21484
|
+
return "invalid request: widgetId required";
|
|
21485
|
+
if (typeof req.domain !== "string" || !req.domain)
|
|
21486
|
+
return "invalid request: domain required";
|
|
21487
|
+
if (typeof req.action !== "string" || !req.action)
|
|
21488
|
+
return "invalid request: action required";
|
|
21489
|
+
return null;
|
|
21490
|
+
}
|
|
21491
|
+
|
|
21492
|
+
/**
|
|
21493
|
+
* Request user approval for an out-of-grant call. Returns a promise
|
|
21494
|
+
* that resolves with the user's decision or rejects on timeout / bad
|
|
21495
|
+
* input.
|
|
21496
|
+
*
|
|
21497
|
+
* decision shape (resolved value):
|
|
21498
|
+
* { approve: true, scope: "once" | "tool" | "parent" | "custom", ...extras }
|
|
21499
|
+
* { approve: false, reason?: string }
|
|
21500
|
+
*
|
|
21501
|
+
* `scope` informs the caller how to write the resulting grant.
|
|
21502
|
+
*/
|
|
21503
|
+
function requestApproval$1(req, opts = {}) {
|
|
21504
|
+
const validation = validateRequest(req);
|
|
21505
|
+
if (validation) {
|
|
21506
|
+
return Promise.reject(new Error(validation));
|
|
21507
|
+
}
|
|
21508
|
+
|
|
21509
|
+
const timeoutMs = Number.isFinite(opts.timeoutMs)
|
|
21510
|
+
? opts.timeoutMs
|
|
21511
|
+
: DEFAULT_TIMEOUT_MS;
|
|
21512
|
+
|
|
21513
|
+
// If a prompt for the same coalesce key is already pending, join it.
|
|
21514
|
+
const key = coalesceKeyOf(req);
|
|
21515
|
+
if (_coalesce.has(key)) {
|
|
21516
|
+
const existingId = _coalesce.get(key);
|
|
21517
|
+
const existing = _pending.get(existingId);
|
|
21518
|
+
if (existing) {
|
|
21519
|
+
return new Promise((resolve, reject) => {
|
|
21520
|
+
existing.joinedResolvers.push({ resolve, reject });
|
|
21521
|
+
});
|
|
21522
|
+
}
|
|
21523
|
+
// Stale coalesce entry; drop and fall through to a fresh request.
|
|
21524
|
+
_coalesce.delete(key);
|
|
21525
|
+
}
|
|
21526
|
+
|
|
21527
|
+
return new Promise((resolve, reject) => {
|
|
21528
|
+
const requestId = nextRequestId();
|
|
21529
|
+
const timeout = setTimeout(() => {
|
|
21530
|
+
const entry = _pending.get(requestId);
|
|
21531
|
+
if (!entry) return;
|
|
21532
|
+
_pending.delete(requestId);
|
|
21533
|
+
_coalesce.delete(entry.coalesceKey);
|
|
21534
|
+
const err = new Error(
|
|
21535
|
+
`JIT consent timed out for ${req.widgetId} (${req.domain}/${req.action}) after ${timeoutMs}ms`,
|
|
21536
|
+
);
|
|
21537
|
+
reject(err);
|
|
21538
|
+
for (const j of entry.joinedResolvers) j.reject(err);
|
|
21539
|
+
}, timeoutMs);
|
|
21540
|
+
|
|
21541
|
+
_pending.set(requestId, {
|
|
21542
|
+
resolve,
|
|
21543
|
+
reject,
|
|
21544
|
+
timeout,
|
|
21545
|
+
coalesceKey: key,
|
|
21546
|
+
joinedResolvers: [],
|
|
21547
|
+
});
|
|
21548
|
+
_coalesce.set(key, requestId);
|
|
21549
|
+
|
|
21550
|
+
emitEvent({
|
|
21551
|
+
requestId,
|
|
21552
|
+
widgetId: req.widgetId,
|
|
21553
|
+
domain: req.domain,
|
|
21554
|
+
action: req.action,
|
|
21555
|
+
args: req.args || {},
|
|
21556
|
+
});
|
|
21557
|
+
});
|
|
21558
|
+
}
|
|
21559
|
+
|
|
21560
|
+
function _handleResponse({ requestId, decision } = {}) {
|
|
21561
|
+
if (!requestId || typeof requestId !== "string") return;
|
|
21562
|
+
const entry = _pending.get(requestId);
|
|
21563
|
+
if (!entry) return; // unknown request — drop silently
|
|
21564
|
+
clearTimeout(entry.timeout);
|
|
21565
|
+
_pending.delete(requestId);
|
|
21566
|
+
_coalesce.delete(entry.coalesceKey);
|
|
21567
|
+
const safe =
|
|
21568
|
+
decision && typeof decision === "object" ? decision : { approve: false };
|
|
21569
|
+
entry.resolve(safe);
|
|
21570
|
+
for (const j of entry.joinedResolvers) j.resolve(safe);
|
|
21571
|
+
}
|
|
21572
|
+
|
|
21573
|
+
function _resetForTest() {
|
|
21574
|
+
for (const entry of _pending.values()) clearTimeout(entry.timeout);
|
|
21575
|
+
_pending.clear();
|
|
21576
|
+
_coalesce.clear();
|
|
21577
|
+
_idCounter = 0;
|
|
21578
|
+
}
|
|
21579
|
+
|
|
21580
|
+
let _handlersRegistered = false;
|
|
21581
|
+
/**
|
|
21582
|
+
* Wire the renderer→main response IPC. Idempotent.
|
|
21583
|
+
* Call once from main.js alongside other ipcMain setup.
|
|
21584
|
+
*/
|
|
21585
|
+
function setupJitConsentHandlers() {
|
|
21586
|
+
if (_handlersRegistered) return;
|
|
21587
|
+
if (!ipcMain$2 || typeof ipcMain$2.on !== "function") return;
|
|
21588
|
+
ipcMain$2.on(RESPONSE_CHANNEL, (_event, payload) => {
|
|
21589
|
+
_handleResponse(payload);
|
|
21590
|
+
});
|
|
21591
|
+
_handlersRegistered = true;
|
|
21592
|
+
}
|
|
21593
|
+
|
|
21594
|
+
var jitConsent$1 = {
|
|
21595
|
+
requestApproval: requestApproval$1,
|
|
21596
|
+
setupJitConsentHandlers,
|
|
21597
|
+
_handleResponse,
|
|
21598
|
+
_resetForTest,
|
|
21599
|
+
REQUEST_CHANNEL,
|
|
21600
|
+
RESPONSE_CHANNEL,
|
|
21601
|
+
DEFAULT_TIMEOUT_MS,
|
|
21602
|
+
};
|
|
21603
|
+
|
|
21382
21604
|
/**
|
|
21383
21605
|
* permissionGate.js
|
|
21384
21606
|
*
|
|
@@ -21418,8 +21640,9 @@ var grantedPermissions = {
|
|
|
21418
21640
|
* dispatch goes through this gate.
|
|
21419
21641
|
*/
|
|
21420
21642
|
|
|
21421
|
-
const { getGrant: getGrant$1 } = grantedPermissions;
|
|
21643
|
+
const { getGrant: getGrant$1, setGrant: setGrant$1 } = grantedPermissions;
|
|
21422
21644
|
const { safePath: safePath$1 } = safePath_1;
|
|
21645
|
+
const { requestApproval } = jitConsent$1;
|
|
21423
21646
|
|
|
21424
21647
|
// Argument keys that look like paths. Different MCP servers use
|
|
21425
21648
|
// different conventions; this list covers the common filesystem-style
|
|
@@ -21535,8 +21758,148 @@ function gateToolCall$1({ widgetId, serverName, toolName, args }) {
|
|
|
21535
21758
|
return { allow: true };
|
|
21536
21759
|
}
|
|
21537
21760
|
|
|
21761
|
+
/**
|
|
21762
|
+
* Heuristic — "no grant" is the only denial reason JIT can recover.
|
|
21763
|
+
* Other denials (missing widgetId, malformed args, server-not-declared
|
|
21764
|
+
* post-grant, path-traversal-rejected) are bugs or attempted abuse,
|
|
21765
|
+
* not consent gaps; the user shouldn't be prompted about those.
|
|
21766
|
+
*/
|
|
21767
|
+
function _isNoGrantDenial(reason) {
|
|
21768
|
+
return (
|
|
21769
|
+
typeof reason === "string" && /no MCP permissions granted/i.test(reason)
|
|
21770
|
+
);
|
|
21771
|
+
}
|
|
21772
|
+
|
|
21773
|
+
/**
|
|
21774
|
+
* Merge `addition` (a grant blob) into the widget's existing grant. Used
|
|
21775
|
+
* by the JIT path to extend an existing grant with a new tool/path
|
|
21776
|
+
* without clobbering grants for other servers.
|
|
21777
|
+
*/
|
|
21778
|
+
function _mergeGrant(current, addition) {
|
|
21779
|
+
const out = {
|
|
21780
|
+
grantOrigin: addition.grantOrigin || current?.grantOrigin || null,
|
|
21781
|
+
servers: { ...(current?.servers || {}) },
|
|
21782
|
+
};
|
|
21783
|
+
for (const [name, perms] of Object.entries(addition.servers || {})) {
|
|
21784
|
+
const existing = out.servers[name] || {
|
|
21785
|
+
tools: [],
|
|
21786
|
+
readPaths: [],
|
|
21787
|
+
writePaths: [],
|
|
21788
|
+
};
|
|
21789
|
+
out.servers[name] = {
|
|
21790
|
+
tools: [...new Set([...(existing.tools || []), ...(perms.tools || [])])],
|
|
21791
|
+
readPaths: [
|
|
21792
|
+
...new Set([...(existing.readPaths || []), ...(perms.readPaths || [])]),
|
|
21793
|
+
],
|
|
21794
|
+
writePaths: [
|
|
21795
|
+
...new Set([
|
|
21796
|
+
...(existing.writePaths || []),
|
|
21797
|
+
...(perms.writePaths || []),
|
|
21798
|
+
]),
|
|
21799
|
+
],
|
|
21800
|
+
};
|
|
21801
|
+
}
|
|
21802
|
+
return out;
|
|
21803
|
+
}
|
|
21804
|
+
|
|
21805
|
+
/**
|
|
21806
|
+
* Async wrapper around gateToolCall that escalates "no grant" denials
|
|
21807
|
+
* to a just-in-time consent prompt when `opts.enableJit` is true.
|
|
21808
|
+
* On approval, the user's chosen grant shape (carried on
|
|
21809
|
+
* `decision.granted`) is merged into the persisted grant and the gate
|
|
21810
|
+
* re-evaluates. On denial / timeout / disabled-flag, returns the
|
|
21811
|
+
* synchronous decision unchanged.
|
|
21812
|
+
*
|
|
21813
|
+
* @returns {Promise<{ allow: true } | { allow: false, reason: string }>}
|
|
21814
|
+
*/
|
|
21815
|
+
async function gateToolCallWithJit$1(req, opts = {}) {
|
|
21816
|
+
const initial = gateToolCall$1(req);
|
|
21817
|
+
if (initial.allow) return initial;
|
|
21818
|
+
if (!opts.enableJit) return initial;
|
|
21819
|
+
if (!_isNoGrantDenial(initial.reason)) return initial;
|
|
21820
|
+
|
|
21821
|
+
let decision;
|
|
21822
|
+
try {
|
|
21823
|
+
decision = await requestApproval(
|
|
21824
|
+
{
|
|
21825
|
+
widgetId: req.widgetId,
|
|
21826
|
+
domain: "mcp",
|
|
21827
|
+
action: "callTool",
|
|
21828
|
+
args: {
|
|
21829
|
+
serverName: req.serverName,
|
|
21830
|
+
toolName: req.toolName,
|
|
21831
|
+
args: req.args || {},
|
|
21832
|
+
},
|
|
21833
|
+
},
|
|
21834
|
+
{ timeoutMs: opts.timeoutMs },
|
|
21835
|
+
);
|
|
21836
|
+
} catch (e) {
|
|
21837
|
+
return {
|
|
21838
|
+
allow: false,
|
|
21839
|
+
reason:
|
|
21840
|
+
"JIT consent " +
|
|
21841
|
+
(e && e.message ? e.message : "failed") +
|
|
21842
|
+
"; original denial: " +
|
|
21843
|
+
initial.reason,
|
|
21844
|
+
};
|
|
21845
|
+
}
|
|
21846
|
+
|
|
21847
|
+
if (!decision || decision.approve !== true) {
|
|
21848
|
+
return {
|
|
21849
|
+
allow: false,
|
|
21850
|
+
reason:
|
|
21851
|
+
"user declined JIT consent for widget '" +
|
|
21852
|
+
req.widgetId +
|
|
21853
|
+
"' calling '" +
|
|
21854
|
+
req.toolName +
|
|
21855
|
+
"' on '" +
|
|
21856
|
+
req.serverName +
|
|
21857
|
+
"'",
|
|
21858
|
+
};
|
|
21859
|
+
}
|
|
21860
|
+
|
|
21861
|
+
// The renderer is expected to carry the chosen grant shape on
|
|
21862
|
+
// decision.granted. Fall back to a minimal tool-level grant if the
|
|
21863
|
+
// shape is missing — never silently grant paths the user didn't
|
|
21864
|
+
// explicitly approve.
|
|
21865
|
+
const addition =
|
|
21866
|
+
decision.granted && typeof decision.granted === "object"
|
|
21867
|
+
? decision.granted
|
|
21868
|
+
: {
|
|
21869
|
+
grantOrigin: "live",
|
|
21870
|
+
servers: {
|
|
21871
|
+
[req.serverName]: {
|
|
21872
|
+
tools: [req.toolName],
|
|
21873
|
+
readPaths: [],
|
|
21874
|
+
writePaths: [],
|
|
21875
|
+
},
|
|
21876
|
+
},
|
|
21877
|
+
};
|
|
21878
|
+
// Force grantOrigin: "live" regardless of what the renderer sent.
|
|
21879
|
+
addition.grantOrigin = "live";
|
|
21880
|
+
|
|
21881
|
+
try {
|
|
21882
|
+
const current = getGrant$1(req.widgetId);
|
|
21883
|
+
const merged = _mergeGrant(current, addition);
|
|
21884
|
+
setGrant$1(req.widgetId, merged);
|
|
21885
|
+
} catch (e) {
|
|
21886
|
+
return {
|
|
21887
|
+
allow: false,
|
|
21888
|
+
reason:
|
|
21889
|
+
"JIT consent: failed to persist grant: " +
|
|
21890
|
+
(e && e.message ? e.message : String(e)),
|
|
21891
|
+
};
|
|
21892
|
+
}
|
|
21893
|
+
|
|
21894
|
+
// Re-evaluate against the freshly-persisted grant. If the user's
|
|
21895
|
+
// grant shape didn't actually cover the requested call (e.g. they
|
|
21896
|
+
// approved a different tool or path), the gate denies as usual.
|
|
21897
|
+
return gateToolCall$1(req);
|
|
21898
|
+
}
|
|
21899
|
+
|
|
21538
21900
|
var permissionGate = {
|
|
21539
21901
|
gateToolCall: gateToolCall$1,
|
|
21902
|
+
gateToolCallWithJit: gateToolCallWithJit$1,
|
|
21540
21903
|
isWriteTool,
|
|
21541
21904
|
PATH_ARG_KEYS,
|
|
21542
21905
|
};
|
|
@@ -21718,6 +22081,37 @@ var mcpScopeResolver = {
|
|
|
21718
22081
|
applyPathScopeToCredentials: applyPathScopeToCredentials$1,
|
|
21719
22082
|
};
|
|
21720
22083
|
|
|
22084
|
+
/**
|
|
22085
|
+
* securityFlags.js
|
|
22086
|
+
*
|
|
22087
|
+
* Centralized readers for the two boolean security flags that gate the
|
|
22088
|
+
* MCP allowlist stack:
|
|
22089
|
+
* - security.enforceWidgetMcpPermissions
|
|
22090
|
+
* - security.enableJitConsent
|
|
22091
|
+
*
|
|
22092
|
+
* **Default semantics: ON.** A missing settings.json, a missing
|
|
22093
|
+
* `security` block, or an undefined field all yield `true`. Only an
|
|
22094
|
+
* explicit `false` opts out. This is intentional — the security stack
|
|
22095
|
+
* is on by default; users have to actively disable it. The
|
|
22096
|
+
* Privacy & Security panel surfaces the toggles + a confirm-on-disable
|
|
22097
|
+
* dialog so the disable path is deliberate.
|
|
22098
|
+
*
|
|
22099
|
+
* The readers are pure functions of a settings object so the
|
|
22100
|
+
* default-on semantics are pinned by unit tests without touching the
|
|
22101
|
+
* filesystem. The callers in mcpController.js wrap these with
|
|
22102
|
+
* settings.json IO.
|
|
22103
|
+
*/
|
|
22104
|
+
|
|
22105
|
+
function readEnforceFlag$1(settings) {
|
|
22106
|
+
return settings?.security?.enforceWidgetMcpPermissions !== false;
|
|
22107
|
+
}
|
|
22108
|
+
|
|
22109
|
+
function readJitFlag$1(settings) {
|
|
22110
|
+
return settings?.security?.enableJitConsent !== false;
|
|
22111
|
+
}
|
|
22112
|
+
|
|
22113
|
+
var securityFlags = { readEnforceFlag: readEnforceFlag$1, readJitFlag: readJitFlag$1 };
|
|
22114
|
+
|
|
21721
22115
|
/**
|
|
21722
22116
|
* mcpController.js
|
|
21723
22117
|
*
|
|
@@ -21742,31 +22136,47 @@ const path$e = require$$1$2;
|
|
|
21742
22136
|
const fs$a = require$$0$2;
|
|
21743
22137
|
const os$2 = require$$2$1;
|
|
21744
22138
|
const responseCache$2 = responseCache_1;
|
|
21745
|
-
const { gateToolCall } = permissionGate;
|
|
22139
|
+
const { gateToolCall, gateToolCallWithJit } = permissionGate;
|
|
21746
22140
|
const { serverKey, parseServerKey } = mcpServerKey;
|
|
21747
22141
|
const { applyPathScopeToCredentials } = mcpScopeResolver;
|
|
22142
|
+
const { readEnforceFlag, readJitFlag } = securityFlags;
|
|
21748
22143
|
const { app: app$7 } = require$$0$1;
|
|
21749
22144
|
|
|
21750
|
-
|
|
21751
|
-
|
|
21752
|
-
|
|
21753
|
-
|
|
21754
|
-
|
|
22145
|
+
/**
|
|
22146
|
+
* Load the user's settings.json (or null on absence/parse error). The
|
|
22147
|
+
* file is small; reading it on every MCP call is acceptable. If we
|
|
22148
|
+
* ever care about overhead, cache + invalidate-on-change.
|
|
22149
|
+
*/
|
|
22150
|
+
function loadSettingsForFlags() {
|
|
21755
22151
|
try {
|
|
21756
22152
|
const settingsPath = path$e.join(
|
|
21757
22153
|
app$7.getPath("userData"),
|
|
21758
22154
|
"Dashboard",
|
|
21759
22155
|
"settings.json",
|
|
21760
22156
|
);
|
|
21761
|
-
if (!fs$a.existsSync(settingsPath)) return
|
|
22157
|
+
if (!fs$a.existsSync(settingsPath)) return null;
|
|
21762
22158
|
const raw = fs$a.readFileSync(settingsPath, "utf8");
|
|
21763
|
-
|
|
21764
|
-
return Boolean(settings?.security?.enforceWidgetMcpPermissions);
|
|
22159
|
+
return JSON.parse(raw);
|
|
21765
22160
|
} catch (_e) {
|
|
21766
|
-
return
|
|
22161
|
+
return null;
|
|
21767
22162
|
}
|
|
21768
22163
|
}
|
|
21769
22164
|
|
|
22165
|
+
// MCP enforcement flag. **Default ON** — only an explicit
|
|
22166
|
+
// `security.enforceWidgetMcpPermissions: false` in settings.json opts
|
|
22167
|
+
// out. The Privacy & Security panel surfaces a UI toggle with a
|
|
22168
|
+
// confirm-on-disable dialog. See electron/utils/securityFlags.js for
|
|
22169
|
+
// the pinned default semantics.
|
|
22170
|
+
function isWidgetPermissionEnforcementEnabled() {
|
|
22171
|
+
return readEnforceFlag(loadSettingsForFlags());
|
|
22172
|
+
}
|
|
22173
|
+
|
|
22174
|
+
// JIT consent flag. **Default ON.** Same semantics as the enforcement
|
|
22175
|
+
// flag — explicit false to opt out, otherwise on.
|
|
22176
|
+
function isJitConsentEnabled() {
|
|
22177
|
+
return readJitFlag(loadSettingsForFlags());
|
|
22178
|
+
}
|
|
22179
|
+
|
|
21770
22180
|
/**
|
|
21771
22181
|
* Tool name prefixes considered safe to cache (read-only).
|
|
21772
22182
|
* Writes/mutations are NOT cached so they always hit the source.
|
|
@@ -21901,7 +22311,7 @@ function getShellPath$1() {
|
|
|
21901
22311
|
return _shellPath$1;
|
|
21902
22312
|
}
|
|
21903
22313
|
|
|
21904
|
-
const { execSync } = require$$
|
|
22314
|
+
const { execSync } = require$$12;
|
|
21905
22315
|
const fallbackDirs = ["/usr/local/bin", "/opt/homebrew/bin"];
|
|
21906
22316
|
|
|
21907
22317
|
// Scan nvm versions, tracking both latest and best compatible version
|
|
@@ -22530,16 +22940,19 @@ const mcpController$3 = {
|
|
|
22530
22940
|
|
|
22531
22941
|
// Per-widget manifest gate. Activated by the
|
|
22532
22942
|
// security.enforceWidgetMcpPermissions setting. When enabled
|
|
22533
|
-
// and a widgetId is supplied, the widget's
|
|
22534
|
-
//
|
|
22535
|
-
//
|
|
22943
|
+
// and a widgetId is supplied, the widget's persisted grant
|
|
22944
|
+
// determines what tools and paths are allowed.
|
|
22945
|
+
//
|
|
22946
|
+
// JIT consent: when security.enableJitConsent is also on, the
|
|
22947
|
+
// gate escalates "no grant" denials into a runtime prompt
|
|
22948
|
+
// (jitConsent.requestApproval → renderer modal → grant write +
|
|
22949
|
+
// re-evaluate). Other denial reasons (path traversal, malformed
|
|
22950
|
+
// args, etc.) stay synchronous.
|
|
22536
22951
|
if (isWidgetPermissionEnforcementEnabled() && widgetId) {
|
|
22537
|
-
const
|
|
22538
|
-
|
|
22539
|
-
|
|
22540
|
-
|
|
22541
|
-
args,
|
|
22542
|
-
});
|
|
22952
|
+
const gateReq = { widgetId, serverName, toolName, args };
|
|
22953
|
+
const gate = isJitConsentEnabled()
|
|
22954
|
+
? await gateToolCallWithJit(gateReq, { enableJit: true })
|
|
22955
|
+
: gateToolCall(gateReq);
|
|
22543
22956
|
if (!gate.allow) {
|
|
22544
22957
|
throw new Error(`Widget permission gate: ${gate.reason}`);
|
|
22545
22958
|
}
|
|
@@ -22855,7 +23268,7 @@ const mcpController$3 = {
|
|
|
22855
23268
|
* @returns {{ success } | { error, message }}
|
|
22856
23269
|
*/
|
|
22857
23270
|
runAuth: async (win, mcpConfig, credentials, authCommand) => {
|
|
22858
|
-
const { spawn } = require$$
|
|
23271
|
+
const { spawn } = require$$12;
|
|
22859
23272
|
|
|
22860
23273
|
const env = cleanEnvForChildProcess();
|
|
22861
23274
|
|
|
@@ -48255,7 +48668,7 @@ var mcpDashServerController_1 = mcpDashServerController$4;
|
|
|
48255
48668
|
* can use the Chat widget without a separate API key.
|
|
48256
48669
|
*/
|
|
48257
48670
|
|
|
48258
|
-
const { spawn, execSync } = require$$
|
|
48671
|
+
const { spawn, execSync } = require$$12;
|
|
48259
48672
|
const {
|
|
48260
48673
|
LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
|
|
48261
48674
|
LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
|
|
@@ -65573,6 +65986,7 @@ const webSocketController = webSocketController_1;
|
|
|
65573
65986
|
const extractionCacheController = extractionCacheController_1;
|
|
65574
65987
|
const mcpDashServerController = mcpDashServerController_1;
|
|
65575
65988
|
const widgetMcpGrantsController = widgetMcpGrantsController$1;
|
|
65989
|
+
const jitConsent = jitConsent$1;
|
|
65576
65990
|
|
|
65577
65991
|
// --- Errors ---
|
|
65578
65992
|
const themeFromUrlErrors = themeFromUrlErrors$1;
|
|
@@ -65678,6 +66092,7 @@ var electron = {
|
|
|
65678
66092
|
extractionCacheController,
|
|
65679
66093
|
mcpDashServerController,
|
|
65680
66094
|
widgetMcpGrantsController,
|
|
66095
|
+
jitConsent,
|
|
65681
66096
|
|
|
65682
66097
|
// Controller functions (flat) — spread for convenient destructuring
|
|
65683
66098
|
...controllers,
|