@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.
@@ -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$$11 = require('child_process');
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. "declared" means the user
21239
- // approved against the developer's declared dash.permissions.mcp block;
21240
- // "discovered" means the install-time scanner produced a synthetic
21241
- // manifest the user approved; "manual" means the user typed entries
21242
- // themselves in Settings → Privacy & Security with no manifest backing.
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(["declared", "discovered", "manual"]);
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$1(widgetId, perms) {
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$1,
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
- // Read the widget-MCP-enforcement feature flag from settings.json.
21751
- // Default is OFF flipping ON activates per-widget gating in
21752
- // permissionGate.gateToolCall(). See docs/security/ipc-filesystem-audit.md
21753
- // and electron/mcp/permissionGate.js for context.
21754
- function isWidgetPermissionEnforcementEnabled() {
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 false;
22157
+ if (!fs$a.existsSync(settingsPath)) return null;
21762
22158
  const raw = fs$a.readFileSync(settingsPath, "utf8");
21763
- const settings = JSON.parse(raw);
21764
- return Boolean(settings?.security?.enforceWidgetMcpPermissions);
22159
+ return JSON.parse(raw);
21765
22160
  } catch (_e) {
21766
- return false;
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$$11;
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 installed
22534
- // package.json's dash.permissions.mcp block determines what
22535
- // tools and paths are allowed.
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 gate = gateToolCall({
22538
- widgetId,
22539
- serverName,
22540
- toolName,
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$$11;
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$$11;
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,