@trops/dash-core 0.1.637 → 0.1.639

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.
@@ -21,7 +21,7 @@ var require$$11 = require('https');
21
21
  var require$$0$8 = require('@modelcontextprotocol/sdk/client/index.js');
22
22
  var require$$1$6 = require('@modelcontextprotocol/sdk/client/stdio.js');
23
23
  var require$$0$7 = require('pkce-challenge');
24
- var require$$12 = require('child_process');
24
+ var require$$13 = require('child_process');
25
25
  var require$$1$5 = require('http');
26
26
  var require$$2$2 = require('algoliasearch');
27
27
  var require$$3$2 = require('node:path');
@@ -982,6 +982,9 @@ const BOTS_RESUME_ALL$1 = "bots-resume-all";
982
982
  const BOTS_PAUSE_BOT$1 = "bots-pause-bot";
983
983
  const BOTS_RESUME_BOT$1 = "bots-resume-bot";
984
984
  const BOTS_GET_PAUSE_STATE$1 = "bots-get-pause-state";
985
+ const BOTS_LIST_TOOL_SOURCES$1 = "bots-list-tool-sources";
986
+ const BOTS_GET_GRANTS$1 = "bots-get-grants";
987
+ const BOTS_REVOKE_GRANT$1 = "bots-revoke-grant";
985
988
 
986
989
  // --- Main → Renderer (send) ---
987
990
  const BOT_STREAM$2 = "bot-stream"; // { botId, event: BotEvent }
@@ -1008,6 +1011,9 @@ var botEvents$1 = {
1008
1011
  BOTS_PAUSE_BOT: BOTS_PAUSE_BOT$1,
1009
1012
  BOTS_RESUME_BOT: BOTS_RESUME_BOT$1,
1010
1013
  BOTS_GET_PAUSE_STATE: BOTS_GET_PAUSE_STATE$1,
1014
+ BOTS_LIST_TOOL_SOURCES: BOTS_LIST_TOOL_SOURCES$1,
1015
+ BOTS_GET_GRANTS: BOTS_GET_GRANTS$1,
1016
+ BOTS_REVOKE_GRANT: BOTS_REVOKE_GRANT$1,
1011
1017
  BOT_STREAM: BOT_STREAM$2,
1012
1018
  BOT_APPROVAL_PENDING: BOT_APPROVAL_PENDING$2,
1013
1019
  BOT_BUDGET_ALERT: BOT_BUDGET_ALERT$2,
@@ -19490,7 +19496,7 @@ util$E.isObject = isObject;
19490
19496
  util$E.isPlainObject = isPlainObject;
19491
19497
  util$E.numKeys = numKeys;
19492
19498
  util$E.escapeRegex = escapeRegex;
19493
- util$E.clone = clone;
19499
+ util$E.clone = clone$1;
19494
19500
  util$E.normalizeParams = normalizeParams;
19495
19501
  util$E.createTransparentProxy = createTransparentProxy;
19496
19502
  util$E.stringifyPrimitive = stringifyPrimitive;
@@ -19713,7 +19719,7 @@ function escapeRegex(str) {
19713
19719
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19714
19720
  }
19715
19721
  // zod-specific utils
19716
- function clone(inst, def, params) {
19722
+ function clone$1(inst, def, params) {
19717
19723
  const cl = new inst._zod.constr(def ?? inst._zod.def);
19718
19724
  if (!def || params?.parent)
19719
19725
  cl._zod.parent = inst;
@@ -19803,7 +19809,7 @@ function pick$1(schema, mask) {
19803
19809
  // pick key
19804
19810
  newShape[key] = currDef.shape[key];
19805
19811
  }
19806
- return clone(schema, {
19812
+ return clone$1(schema, {
19807
19813
  ...schema._zod.def,
19808
19814
  shape: newShape,
19809
19815
  checks: [],
@@ -19820,7 +19826,7 @@ function omit(schema, mask) {
19820
19826
  continue;
19821
19827
  delete newShape[key];
19822
19828
  }
19823
- return clone(schema, {
19829
+ return clone$1(schema, {
19824
19830
  ...schema._zod.def,
19825
19831
  shape: newShape,
19826
19832
  checks: [],
@@ -19839,10 +19845,10 @@ function extend(schema, shape) {
19839
19845
  },
19840
19846
  checks: [], // delete existing checks
19841
19847
  };
19842
- return clone(schema, def);
19848
+ return clone$1(schema, def);
19843
19849
  }
19844
19850
  function merge(a, b) {
19845
- return clone(a, {
19851
+ return clone$1(a, {
19846
19852
  ...a._zod.def,
19847
19853
  get shape() {
19848
19854
  const _shape = { ...a._zod.def.shape, ...b._zod.def.shape };
@@ -19883,7 +19889,7 @@ function partial(Class, schema, mask) {
19883
19889
  : oldShape[key];
19884
19890
  }
19885
19891
  }
19886
- return clone(schema, {
19892
+ return clone$1(schema, {
19887
19893
  ...schema._zod.def,
19888
19894
  shape,
19889
19895
  checks: [],
@@ -19915,7 +19921,7 @@ function required$3(Class, schema, mask) {
19915
19921
  });
19916
19922
  }
19917
19923
  }
19918
- return clone(schema, {
19924
+ return clone$1(schema, {
19919
19925
  ...schema._zod.def,
19920
19926
  shape,
19921
19927
  // optional: [],
@@ -36672,7 +36678,7 @@ function _resolveIdentity({ token, widgetId }) {
36672
36678
  // Argument keys that look like paths. Different MCP servers use
36673
36679
  // different conventions; this list covers the common filesystem-style
36674
36680
  // servers. Extensible — add as new patterns surface.
36675
- const PATH_ARG_KEYS = ["path", "uri", "filepath", "file", "directory"];
36681
+ const PATH_ARG_KEYS$1 = ["path", "uri", "filepath", "file", "directory"];
36676
36682
 
36677
36683
  // Heuristic: tool names matching this regex are treated as writes for
36678
36684
  // purposes of choosing readPaths vs writePaths. The match is intentionally
@@ -36681,7 +36687,7 @@ const PATH_ARG_KEYS = ["path", "uri", "filepath", "file", "directory"];
36681
36687
  const WRITE_TOOL_PATTERN =
36682
36688
  /(^|_)(write|create|edit|delete|remove|append|move|rename|chmod|chown|mkdir)/i;
36683
36689
 
36684
- function isWriteTool(toolName) {
36690
+ function isWriteTool$1(toolName) {
36685
36691
  if (typeof toolName !== "string") return false;
36686
36692
  return WRITE_TOOL_PATTERN.test(toolName);
36687
36693
  }
@@ -36755,7 +36761,7 @@ function _evaluateGrant({
36755
36761
 
36756
36762
  // Path-argument containment. Only checked when the tool's args
36757
36763
  // include a path-shaped key.
36758
- const isWrite = isWriteTool(toolName);
36764
+ const isWrite = isWriteTool$1(toolName);
36759
36765
  // Write tools must use writePaths; read tools may use either
36760
36766
  // readPaths or writePaths (write access implies read access).
36761
36767
  const allowedPaths = isWrite
@@ -36763,7 +36769,7 @@ function _evaluateGrant({
36763
36769
  : [...serverPerms.readPaths, ...serverPerms.writePaths];
36764
36770
 
36765
36771
  if (args && typeof args === "object") {
36766
- for (const key of PATH_ARG_KEYS) {
36772
+ for (const key of PATH_ARG_KEYS$1) {
36767
36773
  const v = args[key];
36768
36774
  if (typeof v !== "string" || !v) continue;
36769
36775
  if (allowedPaths.length === 0) {
@@ -37132,8 +37138,8 @@ var permissionGate = {
37132
37138
  gateToolCall: gateToolCall$1,
37133
37139
  gateBotToolCall,
37134
37140
  gateToolCallWithJit: gateToolCallWithJit$1,
37135
- isWriteTool,
37136
- PATH_ARG_KEYS,
37141
+ isWriteTool: isWriteTool$1,
37142
+ PATH_ARG_KEYS: PATH_ARG_KEYS$1,
37137
37143
  };
37138
37144
 
37139
37145
  /**
@@ -37164,7 +37170,7 @@ var permissionGate = {
37164
37170
  * the trust boundary appears.
37165
37171
  */
37166
37172
 
37167
- const NO_WORKSPACE = "__no_workspace__";
37173
+ const NO_WORKSPACE$1 = "__no_workspace__";
37168
37174
  const SEP = "::";
37169
37175
 
37170
37176
  function serverKey$1(workspaceId, serverName) {
@@ -37172,11 +37178,11 @@ function serverKey$1(workspaceId, serverName) {
37172
37178
  throw new Error("serverKey: serverName is required");
37173
37179
  }
37174
37180
  const wid =
37175
- typeof workspaceId === "string" && workspaceId ? workspaceId : NO_WORKSPACE;
37181
+ typeof workspaceId === "string" && workspaceId ? workspaceId : NO_WORKSPACE$1;
37176
37182
  return wid + SEP + serverName;
37177
37183
  }
37178
37184
 
37179
- function parseServerKey$1(key) {
37185
+ function parseServerKey$2(key) {
37180
37186
  if (typeof key !== "string") {
37181
37187
  throw new Error("parseServerKey: key must be a string");
37182
37188
  }
@@ -37194,11 +37200,265 @@ function parseServerKey$1(key) {
37194
37200
 
37195
37201
  var mcpServerKey = {
37196
37202
  serverKey: serverKey$1,
37197
- parseServerKey: parseServerKey$1,
37198
- NO_WORKSPACE,
37203
+ parseServerKey: parseServerKey$2,
37204
+ NO_WORKSPACE: NO_WORKSPACE$1,
37199
37205
  SEP,
37200
37206
  };
37201
37207
 
37208
+ /**
37209
+ * toolSources.js — how a bot finds, starts, and uses the user's Dash MCP
37210
+ * providers (the integrations configured under Settings → Providers).
37211
+ *
37212
+ * Portable (no electron import — NFR-006): the MCP controller, provider store
37213
+ * and server map are injected by botController.
37214
+ *
37215
+ * Server reuse goes through the same `mcpController.startServer` factory the
37216
+ * dashboards use: a server already connected — or already starting — for a
37217
+ * given (workspace, server) bucket is reused, never spawned twice. Buckets are
37218
+ * per-workspace on purpose (each workspace's processes carry that workspace's
37219
+ * grants), so a bot uses its OWN bucket (its workspaceId, or the no-workspace
37220
+ * bucket) and never borrows a dashboard workspace's process.
37221
+ */
37222
+
37223
+ const { parseServerKey: parseServerKey$1 } = mcpServerKey;
37224
+
37225
+ const NO_WORKSPACE = "__no_workspace__";
37226
+
37227
+ const normalizeWorkspace = (wid) =>
37228
+ typeof wid === "string" && wid && wid !== NO_WORKSPACE ? wid : null;
37229
+
37230
+ /**
37231
+ * Summarize mcpController's `activeServers` Map (keyed "<workspace>::<server>")
37232
+ * into connected servers with their REAL server name + workspace.
37233
+ */
37234
+ function connectedServersFromMap$1(activeServers, connectedStatus) {
37235
+ const out = [];
37236
+ if (!activeServers || typeof activeServers.entries !== "function") return out;
37237
+ for (const [key, server] of activeServers.entries()) {
37238
+ if (!server || server.status !== connectedStatus) continue;
37239
+ let serverName = key;
37240
+ let workspaceId = null;
37241
+ try {
37242
+ const parsed = parseServerKey$1(key);
37243
+ serverName = parsed.serverName;
37244
+ workspaceId = normalizeWorkspace(parsed.workspaceId);
37245
+ } catch (_) {
37246
+ // Legacy un-prefixed key: treat as the no-workspace bucket.
37247
+ }
37248
+ out.push({
37249
+ serverName,
37250
+ workspaceId,
37251
+ tools: server.tools || [],
37252
+ resources: server.resources || [],
37253
+ status: server.status,
37254
+ });
37255
+ }
37256
+ return out;
37257
+ }
37258
+
37259
+ const inBucket = (server, workspaceId) =>
37260
+ normalizeWorkspace(server.workspaceId) === normalizeWorkspace(workspaceId);
37261
+
37262
+ /**
37263
+ * The bot form's provider list: every configured MCP provider (credential-only
37264
+ * providers give a bot nothing to call, so they're excluded), with whether it's
37265
+ * running in the bot's bucket and its tool count when known.
37266
+ *
37267
+ * `tools` is what the form offers for per-bot narrowing: the provider's
37268
+ * declared `allowedTools` (set in Settings → Providers — known without starting
37269
+ * the server); for a provider with no declared limit, the live tool names if
37270
+ * it's running, else null (every tool, list not known yet). Never credentials.
37271
+ */
37272
+ function listToolSources$1({
37273
+ providers,
37274
+ connected = [],
37275
+ workspaceId = null,
37276
+ } = {}) {
37277
+ if (!Array.isArray(providers)) return [];
37278
+ return providers
37279
+ .filter((p) => p && p.providerClass === "mcp" && p.name)
37280
+ .map((p) => {
37281
+ const live = (connected || []).find(
37282
+ (s) => s.serverName === p.name && inBucket(s, workspaceId),
37283
+ );
37284
+ const declared = Array.isArray(p.allowedTools);
37285
+ const liveNames = live
37286
+ ? (live.tools || []).map((t) => t && t.name).filter(Boolean)
37287
+ : null;
37288
+ return {
37289
+ name: p.name,
37290
+ type: p.type || null,
37291
+ running: !!live,
37292
+ toolCount: live ? (live.tools || []).length : null,
37293
+ declared,
37294
+ tools: declared ? [...p.allowedTools] : liveNames,
37295
+ };
37296
+ })
37297
+ .sort((a, b) => a.name.localeCompare(b.name));
37298
+ }
37299
+
37300
+ /**
37301
+ * The tools a bot may use on one provider: the provider's declared limit
37302
+ * (Settings → Providers) is the ceiling; the bot's own selection narrows within
37303
+ * it. null = no restriction (every tool the server exposes).
37304
+ *
37305
+ * @param {string[]|null|undefined} providerLimit provider.allowedTools
37306
+ * @param {string[]|null|undefined} selection bot.toolSelections[provider]
37307
+ * @returns {string[]|null}
37308
+ */
37309
+ function effectiveAllowedTools(providerLimit, selection) {
37310
+ const limit = Array.isArray(providerLimit) ? providerLimit : null;
37311
+ const sel = Array.isArray(selection) ? selection : null;
37312
+ if (limit && sel) return sel.filter((t) => limit.includes(t));
37313
+ if (limit) return [...limit];
37314
+ if (sel) return [...sel];
37315
+ return null;
37316
+ }
37317
+
37318
+ /**
37319
+ * Before a run: start each of the bot's selected providers that isn't already
37320
+ * connected in the bot's bucket, via the shared startServer factory (which
37321
+ * itself dedupes concurrent starts). Never throws — returns per-provider
37322
+ * failures so the run can surface them instead of silently running without
37323
+ * those tools.
37324
+ */
37325
+ async function ensureBotServers$1({
37326
+ bot,
37327
+ connected = [],
37328
+ getProvider,
37329
+ startServer,
37330
+ }) {
37331
+ const failed = [];
37332
+ const wanted = Array.isArray(bot && bot.mcpServers) ? bot.mcpServers : [];
37333
+ const workspaceId = normalizeWorkspace(bot && bot.workspaceId);
37334
+ for (const serverName of wanted) {
37335
+ const running = (connected || []).some(
37336
+ (s) => s.serverName === serverName && inBucket(s, workspaceId),
37337
+ );
37338
+ if (running) continue;
37339
+ try {
37340
+ // providerController.getProvider returns { provider } or
37341
+ // { error, message }; accept a bare provider too.
37342
+ const result = await getProvider(serverName);
37343
+ if (result && result.error) {
37344
+ failed.push({
37345
+ serverName,
37346
+ message:
37347
+ result.message ||
37348
+ `Provider "${serverName}" was not found in Settings → Providers`,
37349
+ });
37350
+ continue;
37351
+ }
37352
+ const provider = result && result.provider ? result.provider : result;
37353
+ if (
37354
+ !provider ||
37355
+ provider.providerClass !== "mcp" ||
37356
+ !provider.mcpConfig
37357
+ ) {
37358
+ failed.push({
37359
+ serverName,
37360
+ message: `Provider "${serverName}" was not found in Settings → Providers`,
37361
+ });
37362
+ continue;
37363
+ }
37364
+ const res = await startServer(
37365
+ serverName,
37366
+ provider.mcpConfig,
37367
+ provider.credentials || {},
37368
+ workspaceId,
37369
+ );
37370
+ if (res && res.error) {
37371
+ failed.push({
37372
+ serverName,
37373
+ message: res.message || `Provider "${serverName}" failed to start`,
37374
+ });
37375
+ }
37376
+ } catch (err) {
37377
+ failed.push({
37378
+ serverName,
37379
+ message: (err && err.message) || String(err),
37380
+ });
37381
+ }
37382
+ }
37383
+ return { failed };
37384
+ }
37385
+
37386
+ /**
37387
+ * The bot's MCP tools: only its selected providers, only from its own bucket,
37388
+ * and only the tools each provider allows (narrowed by the bot's own
37389
+ * selection). Also returns `allowedFor` — the per-provider whitelist the
37390
+ * caller passes to mcpController.callTool so a disallowed call is rejected
37391
+ * there too (same double enforcement widgets get).
37392
+ *
37393
+ * When `providerLimits` is given it is authoritative and fails closed: a
37394
+ * server with no entry (provider deleted, or provider info unreadable) gets no
37395
+ * tools. Omit it only where no provider info exists at all (unit callers).
37396
+ *
37397
+ * @param {{ bot, connected, providerLimits?: Record<string, string[]|null> }} args
37398
+ */
37399
+ function resolveBotTools$1({ bot, connected = [], providerLimits }) {
37400
+ const wanted = new Set(
37401
+ Array.isArray(bot && bot.mcpServers) ? bot.mcpServers : [],
37402
+ );
37403
+ const selections = (bot && bot.toolSelections) || {};
37404
+ const workspaceId = bot && bot.workspaceId;
37405
+ const tools = [];
37406
+ const toolServer = Object.create(null);
37407
+ const allowedFor = Object.create(null);
37408
+ for (const server of connected || []) {
37409
+ const name = server.serverName;
37410
+ if (!wanted.has(name) || !inBucket(server, workspaceId)) continue;
37411
+ if (providerLimits && !(name in providerLimits)) continue; // fail closed
37412
+ const allowed = effectiveAllowedTools(
37413
+ providerLimits ? providerLimits[name] : undefined,
37414
+ selections[name],
37415
+ );
37416
+ allowedFor[name] = allowed;
37417
+ for (const tool of server.tools || []) {
37418
+ if (allowed && !allowed.includes(tool.name)) continue;
37419
+ tools.push(tool);
37420
+ toolServer[tool.name] = name;
37421
+ }
37422
+ }
37423
+ return { tools, toolServer, allowedFor };
37424
+ }
37425
+
37426
+ /**
37427
+ * Last-line enforcement for a bot tool call. `allowedMap` is the
37428
+ * `allowedFor` from this bot's resolveBotTools. Fails closed: unknown bot,
37429
+ * unknown provider, or a tool outside the whitelist → rejected.
37430
+ *
37431
+ * @returns {{ ok: true, allowed: string[]|null } | { ok: false, message: string }}
37432
+ */
37433
+ function checkToolCall$1(allowedMap, serverName, toolName) {
37434
+ if (
37435
+ !allowedMap ||
37436
+ !Object.prototype.hasOwnProperty.call(allowedMap, serverName)
37437
+ ) {
37438
+ return {
37439
+ ok: false,
37440
+ message: `Tool "${toolName}" is not available to this bot (provider "${serverName}" isn't one of its providers).`,
37441
+ };
37442
+ }
37443
+ const allowed = allowedMap[serverName];
37444
+ if (Array.isArray(allowed) && !allowed.includes(toolName)) {
37445
+ return {
37446
+ ok: false,
37447
+ message: `Tool "${toolName}" is not allowed for this bot on "${serverName}".`,
37448
+ };
37449
+ }
37450
+ return { ok: true, allowed: Array.isArray(allowed) ? allowed : null };
37451
+ }
37452
+
37453
+ var toolSources = {
37454
+ checkToolCall: checkToolCall$1,
37455
+ connectedServersFromMap: connectedServersFromMap$1,
37456
+ listToolSources: listToolSources$1,
37457
+ ensureBotServers: ensureBotServers$1,
37458
+ resolveBotTools: resolveBotTools$1,
37459
+ effectiveAllowedTools,
37460
+ };
37461
+
37202
37462
  /**
37203
37463
  * mcpScopeResolver.js
37204
37464
  *
@@ -37577,6 +37837,7 @@ const os$2 = require$$2;
37577
37837
  const responseCache$2 = responseCache_1;
37578
37838
  const { gateToolCall, gateToolCallWithJit } = permissionGate;
37579
37839
  const { serverKey, parseServerKey } = mcpServerKey;
37840
+ const { connectedServersFromMap } = toolSources;
37580
37841
  const { applyPathScopeToCredentials } = mcpScopeResolver;
37581
37842
  const { readEnforceFlag: readEnforceFlag$1, readJitFlag: readJitFlag$1 } = securityFlags;
37582
37843
  const { app: app$8 } = require$$0$2;
@@ -37750,7 +38011,7 @@ function getShellPath$1() {
37750
38011
  return _shellPath$1;
37751
38012
  }
37752
38013
 
37753
- const { execSync } = require$$12;
38014
+ const { execSync } = require$$13;
37754
38015
  const fallbackDirs = ["/usr/local/bin", "/opt/homebrew/bin"];
37755
38016
 
37756
38017
  // Scan nvm versions, tracking both latest and best compatible version
@@ -38854,24 +39115,16 @@ const mcpController$3 = {
38854
39115
  /**
38855
39116
  * listConnectedServers
38856
39117
  * Returns all connected servers with their cached tool lists.
38857
- * Used by llmController to discover available MCP tools.
39118
+ * Used by bots + the LLM tool picker to discover available MCP tools.
38858
39119
  *
38859
- * @returns {Array<{ serverName, tools, resources, status }>}
39120
+ * `serverName` is the real server name and `workspaceId` its bucket
39121
+ * (null = no workspace) — activeServers is keyed "<workspace>::<server>",
39122
+ * and returning that compound key as `serverName` broke name matching.
39123
+ *
39124
+ * @returns {Array<{ serverName, workspaceId, tools, resources, status }>}
38860
39125
  */
38861
- listConnectedServers: () => {
38862
- const servers = [];
38863
- for (const [serverName, server] of activeServers) {
38864
- if (server.status === STATUS$1.CONNECTED) {
38865
- servers.push({
38866
- serverName,
38867
- tools: server.tools || [],
38868
- resources: server.resources || [],
38869
- status: server.status,
38870
- });
38871
- }
38872
- }
38873
- return servers;
38874
- },
39126
+ listConnectedServers: () =>
39127
+ connectedServersFromMap(activeServers, STATUS$1.CONNECTED),
38875
39128
 
38876
39129
  /**
38877
39130
  * runAuth
@@ -38884,7 +39137,7 @@ const mcpController$3 = {
38884
39137
  * @returns {{ success } | { error, message }}
38885
39138
  */
38886
39139
  runAuth: async (win, mcpConfig, credentials, authCommand) => {
38887
- const { spawn } = require$$12;
39140
+ const { spawn } = require$$13;
38888
39141
 
38889
39142
  const env = cleanEnvForChildProcess();
38890
39143
 
@@ -59749,7 +60002,7 @@ var mcpDashServerController_1 = mcpDashServerController$4;
59749
60002
  * can use the Chat widget without a separate API key.
59750
60003
  */
59751
60004
 
59752
- const { spawn, execSync } = require$$12;
60005
+ const { spawn, execSync } = require$$13;
59753
60006
  const {
59754
60007
  LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
59755
60008
  LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
@@ -73788,6 +74041,10 @@ function withDefaults$1(def = {}) {
73788
74041
  // specific engine id here (e.g. "claude-agent").
73789
74042
  engine: null,
73790
74043
  mcpServers: [],
74044
+ // Per-provider narrowing within each provider's declared tools:
74045
+ // { [providerName]: string[] }. No entry → every tool the provider allows.
74046
+ toolSelections: {},
74047
+ // Tools that skip the approval prompt (NOT the tools the bot may use).
73791
74048
  allowedTools: [],
73792
74049
  approvalPolicy: "ask",
73793
74050
  schedules: [],
@@ -73849,6 +74106,22 @@ function validateBotDefinition$1(def) {
73849
74106
  errors.push(`${field} must be an array`);
73850
74107
  }
73851
74108
  }
74109
+ // toolSelections: { [providerName]: string[] } — per-bot tool narrowing.
74110
+ if (def.toolSelections !== undefined) {
74111
+ const ts = def.toolSelections;
74112
+ const ok =
74113
+ ts &&
74114
+ typeof ts === "object" &&
74115
+ !Array.isArray(ts) &&
74116
+ Object.values(ts).every(
74117
+ (v) => Array.isArray(v) && v.every((t) => typeof t === "string"),
74118
+ );
74119
+ if (!ok) {
74120
+ errors.push(
74121
+ "toolSelections must map provider names to arrays of tool names",
74122
+ );
74123
+ }
74124
+ }
73852
74125
  // Each subscription must name an eventType (PRD FR-009).
73853
74126
  if (Array.isArray(def.subscriptions)) {
73854
74127
  for (const sub of def.subscriptions) {
@@ -74915,6 +75188,9 @@ var agentToolBridge = {
74915
75188
  const { createEventStream } = eventStream;
74916
75189
  const { BOT_MCP_SERVER, buildBotMcpServer } = agentToolBridge;
74917
75190
 
75191
+ // The SDK names bridged tools "mcp__<server>__<tool>".
75192
+ const BRIDGED_PREFIX = `mcp__${BOT_MCP_SERVER}__`;
75193
+
74918
75194
  // Test seam: injected fake `query` implementation (see __setQueryForTest).
74919
75195
  let _queryImpl = null;
74920
75196
 
@@ -74974,7 +75250,27 @@ function _makeCanUseTool(ctx) {
74974
75250
  if (ctx.approvalPolicy === "allow") {
74975
75251
  return { behavior: "allow", updatedInput: input };
74976
75252
  }
74977
- // Otherwise gate through the approval queue (shown in the Activity panel).
75253
+ // Bridged Dash provider tools go through the SAME Dash permission gate as
75254
+ // the tool-loop engine — grants, remembered approvals ("Always allow"),
75255
+ // and the approval queue all apply, keyed by the real provider.
75256
+ if (
75257
+ typeof toolName === "string" &&
75258
+ toolName.startsWith(BRIDGED_PREFIX) &&
75259
+ typeof ctx.requestPermission === "function"
75260
+ ) {
75261
+ const decision = await ctx.requestPermission(
75262
+ toolName.slice(BRIDGED_PREFIX.length),
75263
+ input,
75264
+ );
75265
+ return decision && decision.allow
75266
+ ? { behavior: "allow", updatedInput: input }
75267
+ : {
75268
+ behavior: "deny",
75269
+ message: (decision && decision.reason) || "not permitted",
75270
+ };
75271
+ }
75272
+ // Built-in tools (Bash, Write, …) ask every time via the approval queue
75273
+ // (shown in the Activity panel) — they're never remembered.
74978
75274
  if (typeof ctx.createApproval !== "function") {
74979
75275
  return { behavior: "deny", message: "no approval channel available" };
74980
75276
  }
@@ -74998,18 +75294,29 @@ function _makeCanUseTool(ctx) {
74998
75294
  async function _runAgent(ctx, stream) {
74999
75295
  const query = await _loadQuery();
75000
75296
 
75001
- // Auth: prefer the Claude Code CLI session (zero-config); fall back to the
75002
- // bot's Anthropic API key. NOTE: this sets a process-global env var — fine for
75003
- // the common single-key case; per-run isolation is a follow-up.
75297
+ // Per-run environment for the agent process. Auth: the Claude Code login by
75298
+ // default; a bot's Anthropic API key, when set, goes in THIS run's env only —
75299
+ // never process.env, which would leak it to the rest of the main process and
75300
+ // to other bots.
75301
+ const env = { ...process.env };
75004
75302
  if (ctx.credentials && ctx.credentials.apiKey) {
75005
- process.env.ANTHROPIC_API_KEY = ctx.credentials.apiKey;
75303
+ env.ANTHROPIC_API_KEY = ctx.credentials.apiKey;
75006
75304
  }
75305
+ // No claude.ai connectors from the user's Claude login.
75306
+ env.ENABLE_CLAUDEAI_MCP_SERVERS = "false";
75007
75307
 
75008
75308
  const options = {
75009
75309
  model: ctx.model,
75010
75310
  systemPrompt: ctx.systemPrompt || { type: "preset", preset: "claude_code" },
75011
75311
  permissionMode: "default",
75012
75312
  canUseTool: _makeCanUseTool(ctx),
75313
+ // Isolation: a bot sees only the SDK's built-in tools + the Dash providers
75314
+ // it was granted (bridged below as "bot-mcp"). Don't load the user's
75315
+ // ~/.claude settings/plugins/skills/hooks, and ignore every MCP config
75316
+ // other than the servers passed in `mcpServers`.
75317
+ settingSources: [],
75318
+ strictMcpConfig: true,
75319
+ env,
75013
75320
  };
75014
75321
  if (ctx.maxTurns) options.maxTurns = ctx.maxTurns;
75015
75322
  if (ctx.workingDir) options.cwd = ctx.workingDir;
@@ -75445,12 +75752,17 @@ let BotRunner$1 = class BotRunner {
75445
75752
  }
75446
75753
  return this._callTool(serverName, toolName, input, {
75447
75754
  workspaceId: bot.workspaceId,
75755
+ // Lets the caller apply this bot's per-provider tool limits.
75756
+ botId,
75448
75757
  });
75449
75758
  };
75450
75759
 
75451
- // Resume only when the stored session was produced by this engine.
75760
+ // A run does the bot's job against current data → fresh conversation.
75761
+ // Resume only when explicitly continuing (reply-to-continue) AND the
75762
+ // stored session was produced by this engine. Anything a bot should
75763
+ // carry between runs belongs in its memory tools.
75452
75764
  const session =
75453
- bot.session && bot.session.engine === engine.id
75765
+ opts.continueSession && bot.session && bot.session.engine === engine.id
75454
75766
  ? bot.session.state
75455
75767
  : null;
75456
75768
 
@@ -76253,6 +76565,111 @@ function normalizeMcpResult$1(mcpResult) {
76253
76565
 
76254
76566
  var mcpResult = { normalizeMcpResult: normalizeMcpResult$1 };
76255
76567
 
76568
+ /**
76569
+ * rememberGrant.js — turn an "Always allow" bot approval into a durable grant,
76570
+ * and revoke it.
76571
+ *
76572
+ * Bot grants live in the shared grant store (electron/mcp/grantedPermissions)
76573
+ * keyed by botId, shape `{ servers: { [provider]: { tools, readPaths,
76574
+ * writePaths } } }`, and are enforced by the bot permission gate
76575
+ * (gateBotToolCall). A remembered approval is scoped to this bot + this
76576
+ * provider + this tool, plus — when the tool call carries a path argument —
76577
+ * the folder of that path (write folder for write tools, read folder
76578
+ * otherwise). Nothing broader.
76579
+ *
76580
+ * Pure (NFR-006): the gate's path rules (isWriteTool, path arg keys) and
76581
+ * `dirname` are injected so this matches the real gate exactly.
76582
+ */
76583
+
76584
+ const LISTING_TOOL = /(list|director|tree|search)/i;
76585
+
76586
+ /**
76587
+ * The folder to remember for a path argument: a `directory` arg, or a
76588
+ * directory-listing tool, names the folder itself; any other path is a file,
76589
+ * so its parent folder is remembered.
76590
+ */
76591
+ function folderForPathArg(key, value, toolName, dirname) {
76592
+ if (key === "directory" || LISTING_TOOL.test(String(toolName || ""))) {
76593
+ return value;
76594
+ }
76595
+ return dirname(value);
76596
+ }
76597
+
76598
+ const clone = (perms) =>
76599
+ perms && typeof perms === "object" ? JSON.parse(JSON.stringify(perms)) : {};
76600
+
76601
+ const addUnique = (list, value) =>
76602
+ list.includes(value) ? list : [...list, value];
76603
+
76604
+ /**
76605
+ * @param {object|null} perms the bot's current grant (or null)
76606
+ * @param {{ serverName: string, toolName: string, args?: object }} call
76607
+ * @param {{ isWriteTool: Function, pathArgKeys: string[], dirname: Function }} helpers
76608
+ * @returns {object} the new grant (input is not mutated)
76609
+ */
76610
+ function rememberToolGrant$1(perms, call, helpers) {
76611
+ const { serverName, toolName, args } = call;
76612
+ const next = clone(perms);
76613
+ next.grantOrigin = next.grantOrigin || "manual";
76614
+ next.servers = next.servers || {};
76615
+ const server = next.servers[serverName] || {
76616
+ tools: [],
76617
+ readPaths: [],
76618
+ writePaths: [],
76619
+ };
76620
+ server.tools = addUnique(server.tools || [], toolName);
76621
+ server.readPaths = server.readPaths || [];
76622
+ server.writePaths = server.writePaths || [];
76623
+
76624
+ if (args && typeof args === "object") {
76625
+ const isWrite = helpers.isWriteTool(toolName);
76626
+ for (const key of helpers.pathArgKeys) {
76627
+ const v = args[key];
76628
+ if (typeof v !== "string" || !v) continue;
76629
+ const folder = folderForPathArg(key, v, toolName, helpers.dirname);
76630
+ if (isWrite) server.writePaths = addUnique(server.writePaths, folder);
76631
+ else server.readPaths = addUnique(server.readPaths, folder);
76632
+ }
76633
+ }
76634
+
76635
+ next.servers[serverName] = server;
76636
+ return next;
76637
+ }
76638
+
76639
+ /**
76640
+ * Revoke one remembered tool. When it was the provider's last remembered
76641
+ * tool, the provider entry (and its folders) is removed entirely.
76642
+ */
76643
+ function forgetToolGrant$1(perms, serverName, toolName) {
76644
+ const server = perms && perms.servers && perms.servers[serverName];
76645
+ if (!server || !(server.tools || []).includes(toolName)) return perms;
76646
+ const next = clone(perms);
76647
+ const tools = next.servers[serverName].tools.filter((t) => t !== toolName);
76648
+ if (tools.length) next.servers[serverName].tools = tools;
76649
+ else delete next.servers[serverName];
76650
+ return next;
76651
+ }
76652
+
76653
+ /** Remembered tools + folders per provider, for display in the bot form. */
76654
+ function summarizeGrants$1(perms) {
76655
+ const out = {};
76656
+ const servers = (perms && perms.servers) || {};
76657
+ for (const [name, s] of Object.entries(servers)) {
76658
+ out[name] = {
76659
+ tools: [...(s.tools || [])],
76660
+ folders: [...(s.readPaths || []), ...(s.writePaths || [])],
76661
+ };
76662
+ }
76663
+ return out;
76664
+ }
76665
+
76666
+ var rememberGrant = {
76667
+ rememberToolGrant: rememberToolGrant$1,
76668
+ forgetToolGrant: forgetToolGrant$1,
76669
+ summarizeGrants: summarizeGrants$1,
76670
+ folderForPathArg,
76671
+ };
76672
+
76256
76673
  /**
76257
76674
  * botController.js
76258
76675
  *
@@ -76284,6 +76701,19 @@ const {
76284
76701
  const { matchSubscribedBots } = eventMatcher;
76285
76702
  const { EventDispatcher } = EventDispatcher_1;
76286
76703
  const { normalizeMcpResult } = mcpResult;
76704
+ const {
76705
+ listToolSources,
76706
+ ensureBotServers,
76707
+ resolveBotTools,
76708
+ checkToolCall,
76709
+ } = toolSources;
76710
+ const {
76711
+ rememberToolGrant,
76712
+ forgetToolGrant,
76713
+ summarizeGrants,
76714
+ } = rememberGrant;
76715
+ const grantStore = grantedPermissions;
76716
+ const { isWriteTool, PATH_ARG_KEYS } = permissionGate;
76287
76717
  const {
76288
76718
  getProvider: getProvider$1,
76289
76719
  getDefaultModel,
@@ -76387,6 +76817,10 @@ const botController$1 = {
76387
76817
  // flood the runner with event-triggered runs.
76388
76818
  this._dispatcher = new EventDispatcher();
76389
76819
 
76820
+ // botId → { [providerName]: allowed tool names | null }, set on each run's
76821
+ // tool resolution and enforced in _callTool.
76822
+ this._allowedByBot = new Map();
76823
+
76390
76824
  this._runner = new BotRunner({
76391
76825
  engines,
76392
76826
  store: this._store,
@@ -76472,6 +76906,26 @@ const botController$1 = {
76472
76906
  return this._store.list();
76473
76907
  },
76474
76908
 
76909
+ /**
76910
+ * The user's Dash MCP providers a bot can use (Settings → Providers), with
76911
+ * running status + tool count. Names/types only — never credentials.
76912
+ */
76913
+ listToolSources(workspaceId = null) {
76914
+ let providers = [];
76915
+ try {
76916
+ const win = this._getMainWindow();
76917
+ ({ providers = [] } =
76918
+ this._providers.listProviders(win, this._appId) || {});
76919
+ } catch (_e) {
76920
+ providers = [];
76921
+ }
76922
+ return listToolSources({
76923
+ providers,
76924
+ connected: this._connectedServers(),
76925
+ workspaceId,
76926
+ });
76927
+ },
76928
+
76475
76929
  get(botId) {
76476
76930
  return this._store.get(botId);
76477
76931
  },
@@ -76487,6 +76941,8 @@ const botController$1 = {
76487
76941
 
76488
76942
  delete(botId) {
76489
76943
  this._scheduler.unregister(botId);
76944
+ // A deleted bot's remembered approvals go with it.
76945
+ grantStore.revokeGrant(botId);
76490
76946
  return this._store.delete(botId);
76491
76947
  },
76492
76948
 
@@ -76498,10 +76954,50 @@ const botController$1 = {
76498
76954
  return { stopped: this._runner.abort(botId) };
76499
76955
  },
76500
76956
 
76957
+ /**
76958
+ * Resolve a pending approval. `decision.remember` ("Always allow") also
76959
+ * saves a durable grant for this bot + provider + tool (+ the folder of a
76960
+ * path argument), so later runs use that tool without asking. Only provider
76961
+ * tool approvals can be remembered — built-in agent tools have no provider
76962
+ * and always ask.
76963
+ */
76501
76964
  approve(approvalId, decision = {}) {
76502
- return decision.allow
76503
- ? { ok: this._approvals.resolve(approvalId, decision) }
76504
- : { ok: this._approvals.deny(approvalId, decision.reason) };
76965
+ if (!decision.allow) {
76966
+ return { ok: this._approvals.deny(approvalId, decision.reason) };
76967
+ }
76968
+ let remembered = false;
76969
+ if (decision.remember) {
76970
+ const pending = this._approvals.get(approvalId);
76971
+ const req = pending && pending.request;
76972
+ if (req && req.botId && req.serverName && req.toolName) {
76973
+ const next = rememberToolGrant(
76974
+ grantStore.getGrant(req.botId),
76975
+ {
76976
+ serverName: req.serverName,
76977
+ toolName: req.toolName,
76978
+ args: req.input,
76979
+ },
76980
+ { isWriteTool, pathArgKeys: PATH_ARG_KEYS, dirname: path$2.dirname },
76981
+ );
76982
+ remembered = grantStore.setGrant(req.botId, next) === true;
76983
+ }
76984
+ }
76985
+ return { ok: this._approvals.resolve(approvalId, decision), remembered };
76986
+ },
76987
+
76988
+ /** Remembered approvals for a bot: { [provider]: { tools, folders } }. */
76989
+ getGrants(botId) {
76990
+ return summarizeGrants(grantStore.getGrant(botId));
76991
+ },
76992
+
76993
+ /** Revoke one remembered tool approval; returns the updated summary. */
76994
+ revokeGrant(botId, serverName, toolName) {
76995
+ const current = grantStore.getGrant(botId);
76996
+ if (current) {
76997
+ const next = forgetToolGrant(current, serverName, toolName);
76998
+ if (next !== current) grantStore.setGrant(botId, next);
76999
+ }
77000
+ return this.getGrants(botId);
76505
77001
  },
76506
77002
 
76507
77003
  listApprovals() {
@@ -76638,23 +77134,58 @@ const botController$1 = {
76638
77134
  };
76639
77135
  },
76640
77136
 
76641
- /** Build the bot's tool set + tool→server resolver from connected servers. */
76642
- _resolveTools(bot) {
76643
- const connected = this._mcp.listConnectedServers
77137
+ /** Connected MCP servers with real server names + workspace buckets. */
77138
+ _connectedServers() {
77139
+ return this._mcp && this._mcp.listConnectedServers
76644
77140
  ? this._mcp.listConnectedServers()
76645
77141
  : [];
76646
- const wanted = Array.isArray(bot.mcpServers)
76647
- ? new Set(bot.mcpServers)
76648
- : null;
76649
- const tools = [];
76650
- const toolServer = Object.create(null);
76651
- for (const server of connected) {
76652
- if (wanted && !wanted.has(server.serverName)) continue;
76653
- for (const tool of server.tools || []) {
76654
- tools.push(tool);
76655
- toolServer[tool.name] = server.serverName;
77142
+ },
77143
+
77144
+ /**
77145
+ * Build the bot's tool set + tool→server resolver. First starts any of the
77146
+ * bot's selected providers that aren't running in its bucket — through the
77147
+ * same deduping startServer factory the dashboards use — so a scheduled or
77148
+ * event-triggered bot doesn't silently run without its tools. Start
77149
+ * failures are surfaced in the run feed as warnings.
77150
+ */
77151
+ async _resolveTools(bot) {
77152
+ if (this._mcp && this._mcp.startServer && this._providers) {
77153
+ const win = this._getMainWindow();
77154
+ const { failed } = await ensureBotServers({
77155
+ bot,
77156
+ connected: this._connectedServers(),
77157
+ getProvider: async (name) =>
77158
+ this._providers.getProvider(win, this._appId, name),
77159
+ startServer: (name, mcpConfig, credentials, workspaceId) =>
77160
+ this._mcp.startServer(
77161
+ win,
77162
+ name,
77163
+ mcpConfig,
77164
+ credentials,
77165
+ workspaceId,
77166
+ null,
77167
+ this._appId,
77168
+ ),
77169
+ });
77170
+ for (const f of failed) {
77171
+ this._broadcast(BOT_STREAM$1, {
77172
+ botId: bot.id,
77173
+ event: {
77174
+ type: "warning",
77175
+ message: `${f.serverName} couldn't start, so its tools aren't available this run: ${f.message}`,
77176
+ },
77177
+ });
76656
77178
  }
76657
77179
  }
77180
+ // Each provider's declared tools (Settings → Providers) are the ceiling;
77181
+ // the bot's toolSelections narrow within them.
77182
+ const { tools, toolServer, allowedFor } = resolveBotTools({
77183
+ bot,
77184
+ connected: this._connectedServers(),
77185
+ providerLimits: this._providerToolLimits(),
77186
+ });
77187
+ // Remembered per bot so _callTool can hard-enforce the same whitelist.
77188
+ this._allowedByBot.set(bot.id, allowedFor);
76658
77189
  // Always-available in-process memory tools (P1: FR-010).
76659
77190
  for (const tool of MEMORY_TOOLS) {
76660
77191
  tools.push(tool);
@@ -76674,20 +77205,54 @@ const botController$1 = {
76674
77205
  );
76675
77206
  }
76676
77207
  const win = this._getMainWindow();
77208
+ // The bot's per-provider whitelist (provider limit ∩ bot selection), from
77209
+ // the last _resolveTools for this bot. A tool outside it is rejected —
77210
+ // mirroring the allowedTools enforcement widgets get. If no entry exists
77211
+ // for this provider, the bot never had it resolved → deny.
77212
+ const check = checkToolCall(
77213
+ this._allowedByBot.get(opts.botId),
77214
+ serverName,
77215
+ toolName,
77216
+ );
77217
+ if (!check.ok) return { text: check.message, isError: true };
77218
+ const { allowed } = check;
76677
77219
  // widgetId + token are null → the widget permission gate is bypassed; bot
76678
- // tool calls are gated upstream by the bot PermissionGate.
77220
+ // tool calls are gated upstream by the bot PermissionGate. `allowed` is
77221
+ // also passed as callTool's whitelist (defense in depth).
76679
77222
  const result = await this._mcp.callTool(
76680
77223
  win,
76681
77224
  serverName,
76682
77225
  toolName,
76683
77226
  args,
76684
- null,
77227
+ allowed || null,
76685
77228
  null,
76686
77229
  opts.workspaceId || null,
76687
77230
  null,
76688
77231
  );
76689
77232
  return normalizeMcpResult(result);
76690
77233
  },
77234
+
77235
+ /** { [providerName]: allowedTools | null } for the user's MCP providers. */
77236
+ _providerToolLimits() {
77237
+ const limits = Object.create(null);
77238
+ try {
77239
+ const win = this._getMainWindow();
77240
+ const { providers = [] } =
77241
+ this._providers.listProviders(win, this._appId) || {};
77242
+ for (const p of providers) {
77243
+ if (p && p.providerClass === "mcp" && p.name) {
77244
+ limits[p.name] = Array.isArray(p.allowedTools)
77245
+ ? p.allowedTools
77246
+ : null;
77247
+ }
77248
+ }
77249
+ } catch (_e) {
77250
+ // Provider info unreadable → empty map. resolveBotTools treats a
77251
+ // provider with no entry as not configured, so the bot gets NO tools
77252
+ // from it (fails closed) rather than every tool the server exposes.
77253
+ }
77254
+ return limits;
77255
+ },
76691
77256
  };
76692
77257
 
76693
77258
  var botController_1 = botController$1;
@@ -80600,6 +81165,9 @@ const {
80600
81165
  BOTS_PAUSE_BOT,
80601
81166
  BOTS_RESUME_BOT,
80602
81167
  BOTS_GET_PAUSE_STATE,
81168
+ BOTS_LIST_TOOL_SOURCES,
81169
+ BOTS_GET_GRANTS,
81170
+ BOTS_REVOKE_GRANT,
80603
81171
  BOT_STREAM,
80604
81172
  BOT_APPROVAL_PENDING,
80605
81173
  BOT_BUDGET_ALERT,
@@ -80648,6 +81216,16 @@ const botApi$1 = {
80648
81216
  resumeBot: (botId) => ipcRenderer$3.invoke(BOTS_RESUME_BOT, { botId }),
80649
81217
  getPauseState: () => ipcRenderer$3.invoke(BOTS_GET_PAUSE_STATE),
80650
81218
 
81219
+ /** The user's Dash MCP providers a bot can use: [{ name, type, running, toolCount }]. */
81220
+ listToolSources: (workspaceId = null) =>
81221
+ ipcRenderer$3.invoke(BOTS_LIST_TOOL_SOURCES, { workspaceId }),
81222
+
81223
+ /** Remembered approvals ("Always allow"): { [provider]: { tools, folders } }. */
81224
+ getGrants: (botId) => ipcRenderer$3.invoke(BOTS_GET_GRANTS, { botId }),
81225
+ /** Revoke one remembered tool approval; resolves to the updated summary. */
81226
+ revokeGrant: (botId, serverName, toolName) =>
81227
+ ipcRenderer$3.invoke(BOTS_REVOKE_GRANT, { botId, serverName, toolName }),
81228
+
80651
81229
  /** Subscribe to streamed run events: { botId, event }. */
80652
81230
  onStream: (callback) => _addListener(BOT_STREAM, callback),
80653
81231
  /** Subscribe to new pending approvals: { id, request }. */