@trops/dash-core 0.1.637 → 0.1.638

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,7 @@ 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";
985
986
 
986
987
  // --- Main → Renderer (send) ---
987
988
  const BOT_STREAM$2 = "bot-stream"; // { botId, event: BotEvent }
@@ -1008,6 +1009,7 @@ var botEvents$1 = {
1008
1009
  BOTS_PAUSE_BOT: BOTS_PAUSE_BOT$1,
1009
1010
  BOTS_RESUME_BOT: BOTS_RESUME_BOT$1,
1010
1011
  BOTS_GET_PAUSE_STATE: BOTS_GET_PAUSE_STATE$1,
1012
+ BOTS_LIST_TOOL_SOURCES: BOTS_LIST_TOOL_SOURCES$1,
1011
1013
  BOT_STREAM: BOT_STREAM$2,
1012
1014
  BOT_APPROVAL_PENDING: BOT_APPROVAL_PENDING$2,
1013
1015
  BOT_BUDGET_ALERT: BOT_BUDGET_ALERT$2,
@@ -37164,7 +37166,7 @@ var permissionGate = {
37164
37166
  * the trust boundary appears.
37165
37167
  */
37166
37168
 
37167
- const NO_WORKSPACE = "__no_workspace__";
37169
+ const NO_WORKSPACE$1 = "__no_workspace__";
37168
37170
  const SEP = "::";
37169
37171
 
37170
37172
  function serverKey$1(workspaceId, serverName) {
@@ -37172,11 +37174,11 @@ function serverKey$1(workspaceId, serverName) {
37172
37174
  throw new Error("serverKey: serverName is required");
37173
37175
  }
37174
37176
  const wid =
37175
- typeof workspaceId === "string" && workspaceId ? workspaceId : NO_WORKSPACE;
37177
+ typeof workspaceId === "string" && workspaceId ? workspaceId : NO_WORKSPACE$1;
37176
37178
  return wid + SEP + serverName;
37177
37179
  }
37178
37180
 
37179
- function parseServerKey$1(key) {
37181
+ function parseServerKey$2(key) {
37180
37182
  if (typeof key !== "string") {
37181
37183
  throw new Error("parseServerKey: key must be a string");
37182
37184
  }
@@ -37194,11 +37196,265 @@ function parseServerKey$1(key) {
37194
37196
 
37195
37197
  var mcpServerKey = {
37196
37198
  serverKey: serverKey$1,
37197
- parseServerKey: parseServerKey$1,
37198
- NO_WORKSPACE,
37199
+ parseServerKey: parseServerKey$2,
37200
+ NO_WORKSPACE: NO_WORKSPACE$1,
37199
37201
  SEP,
37200
37202
  };
37201
37203
 
37204
+ /**
37205
+ * toolSources.js — how a bot finds, starts, and uses the user's Dash MCP
37206
+ * providers (the integrations configured under Settings → Providers).
37207
+ *
37208
+ * Portable (no electron import — NFR-006): the MCP controller, provider store
37209
+ * and server map are injected by botController.
37210
+ *
37211
+ * Server reuse goes through the same `mcpController.startServer` factory the
37212
+ * dashboards use: a server already connected — or already starting — for a
37213
+ * given (workspace, server) bucket is reused, never spawned twice. Buckets are
37214
+ * per-workspace on purpose (each workspace's processes carry that workspace's
37215
+ * grants), so a bot uses its OWN bucket (its workspaceId, or the no-workspace
37216
+ * bucket) and never borrows a dashboard workspace's process.
37217
+ */
37218
+
37219
+ const { parseServerKey: parseServerKey$1 } = mcpServerKey;
37220
+
37221
+ const NO_WORKSPACE = "__no_workspace__";
37222
+
37223
+ const normalizeWorkspace = (wid) =>
37224
+ typeof wid === "string" && wid && wid !== NO_WORKSPACE ? wid : null;
37225
+
37226
+ /**
37227
+ * Summarize mcpController's `activeServers` Map (keyed "<workspace>::<server>")
37228
+ * into connected servers with their REAL server name + workspace.
37229
+ */
37230
+ function connectedServersFromMap$1(activeServers, connectedStatus) {
37231
+ const out = [];
37232
+ if (!activeServers || typeof activeServers.entries !== "function") return out;
37233
+ for (const [key, server] of activeServers.entries()) {
37234
+ if (!server || server.status !== connectedStatus) continue;
37235
+ let serverName = key;
37236
+ let workspaceId = null;
37237
+ try {
37238
+ const parsed = parseServerKey$1(key);
37239
+ serverName = parsed.serverName;
37240
+ workspaceId = normalizeWorkspace(parsed.workspaceId);
37241
+ } catch (_) {
37242
+ // Legacy un-prefixed key: treat as the no-workspace bucket.
37243
+ }
37244
+ out.push({
37245
+ serverName,
37246
+ workspaceId,
37247
+ tools: server.tools || [],
37248
+ resources: server.resources || [],
37249
+ status: server.status,
37250
+ });
37251
+ }
37252
+ return out;
37253
+ }
37254
+
37255
+ const inBucket = (server, workspaceId) =>
37256
+ normalizeWorkspace(server.workspaceId) === normalizeWorkspace(workspaceId);
37257
+
37258
+ /**
37259
+ * The bot form's provider list: every configured MCP provider (credential-only
37260
+ * providers give a bot nothing to call, so they're excluded), with whether it's
37261
+ * running in the bot's bucket and its tool count when known.
37262
+ *
37263
+ * `tools` is what the form offers for per-bot narrowing: the provider's
37264
+ * declared `allowedTools` (set in Settings → Providers — known without starting
37265
+ * the server); for a provider with no declared limit, the live tool names if
37266
+ * it's running, else null (every tool, list not known yet). Never credentials.
37267
+ */
37268
+ function listToolSources$1({
37269
+ providers,
37270
+ connected = [],
37271
+ workspaceId = null,
37272
+ } = {}) {
37273
+ if (!Array.isArray(providers)) return [];
37274
+ return providers
37275
+ .filter((p) => p && p.providerClass === "mcp" && p.name)
37276
+ .map((p) => {
37277
+ const live = (connected || []).find(
37278
+ (s) => s.serverName === p.name && inBucket(s, workspaceId),
37279
+ );
37280
+ const declared = Array.isArray(p.allowedTools);
37281
+ const liveNames = live
37282
+ ? (live.tools || []).map((t) => t && t.name).filter(Boolean)
37283
+ : null;
37284
+ return {
37285
+ name: p.name,
37286
+ type: p.type || null,
37287
+ running: !!live,
37288
+ toolCount: live ? (live.tools || []).length : null,
37289
+ declared,
37290
+ tools: declared ? [...p.allowedTools] : liveNames,
37291
+ };
37292
+ })
37293
+ .sort((a, b) => a.name.localeCompare(b.name));
37294
+ }
37295
+
37296
+ /**
37297
+ * The tools a bot may use on one provider: the provider's declared limit
37298
+ * (Settings → Providers) is the ceiling; the bot's own selection narrows within
37299
+ * it. null = no restriction (every tool the server exposes).
37300
+ *
37301
+ * @param {string[]|null|undefined} providerLimit provider.allowedTools
37302
+ * @param {string[]|null|undefined} selection bot.toolSelections[provider]
37303
+ * @returns {string[]|null}
37304
+ */
37305
+ function effectiveAllowedTools(providerLimit, selection) {
37306
+ const limit = Array.isArray(providerLimit) ? providerLimit : null;
37307
+ const sel = Array.isArray(selection) ? selection : null;
37308
+ if (limit && sel) return sel.filter((t) => limit.includes(t));
37309
+ if (limit) return [...limit];
37310
+ if (sel) return [...sel];
37311
+ return null;
37312
+ }
37313
+
37314
+ /**
37315
+ * Before a run: start each of the bot's selected providers that isn't already
37316
+ * connected in the bot's bucket, via the shared startServer factory (which
37317
+ * itself dedupes concurrent starts). Never throws — returns per-provider
37318
+ * failures so the run can surface them instead of silently running without
37319
+ * those tools.
37320
+ */
37321
+ async function ensureBotServers$1({
37322
+ bot,
37323
+ connected = [],
37324
+ getProvider,
37325
+ startServer,
37326
+ }) {
37327
+ const failed = [];
37328
+ const wanted = Array.isArray(bot && bot.mcpServers) ? bot.mcpServers : [];
37329
+ const workspaceId = normalizeWorkspace(bot && bot.workspaceId);
37330
+ for (const serverName of wanted) {
37331
+ const running = (connected || []).some(
37332
+ (s) => s.serverName === serverName && inBucket(s, workspaceId),
37333
+ );
37334
+ if (running) continue;
37335
+ try {
37336
+ // providerController.getProvider returns { provider } or
37337
+ // { error, message }; accept a bare provider too.
37338
+ const result = await getProvider(serverName);
37339
+ if (result && result.error) {
37340
+ failed.push({
37341
+ serverName,
37342
+ message:
37343
+ result.message ||
37344
+ `Provider "${serverName}" was not found in Settings → Providers`,
37345
+ });
37346
+ continue;
37347
+ }
37348
+ const provider = result && result.provider ? result.provider : result;
37349
+ if (
37350
+ !provider ||
37351
+ provider.providerClass !== "mcp" ||
37352
+ !provider.mcpConfig
37353
+ ) {
37354
+ failed.push({
37355
+ serverName,
37356
+ message: `Provider "${serverName}" was not found in Settings → Providers`,
37357
+ });
37358
+ continue;
37359
+ }
37360
+ const res = await startServer(
37361
+ serverName,
37362
+ provider.mcpConfig,
37363
+ provider.credentials || {},
37364
+ workspaceId,
37365
+ );
37366
+ if (res && res.error) {
37367
+ failed.push({
37368
+ serverName,
37369
+ message: res.message || `Provider "${serverName}" failed to start`,
37370
+ });
37371
+ }
37372
+ } catch (err) {
37373
+ failed.push({
37374
+ serverName,
37375
+ message: (err && err.message) || String(err),
37376
+ });
37377
+ }
37378
+ }
37379
+ return { failed };
37380
+ }
37381
+
37382
+ /**
37383
+ * The bot's MCP tools: only its selected providers, only from its own bucket,
37384
+ * and only the tools each provider allows (narrowed by the bot's own
37385
+ * selection). Also returns `allowedFor` — the per-provider whitelist the
37386
+ * caller passes to mcpController.callTool so a disallowed call is rejected
37387
+ * there too (same double enforcement widgets get).
37388
+ *
37389
+ * When `providerLimits` is given it is authoritative and fails closed: a
37390
+ * server with no entry (provider deleted, or provider info unreadable) gets no
37391
+ * tools. Omit it only where no provider info exists at all (unit callers).
37392
+ *
37393
+ * @param {{ bot, connected, providerLimits?: Record<string, string[]|null> }} args
37394
+ */
37395
+ function resolveBotTools$1({ bot, connected = [], providerLimits }) {
37396
+ const wanted = new Set(
37397
+ Array.isArray(bot && bot.mcpServers) ? bot.mcpServers : [],
37398
+ );
37399
+ const selections = (bot && bot.toolSelections) || {};
37400
+ const workspaceId = bot && bot.workspaceId;
37401
+ const tools = [];
37402
+ const toolServer = Object.create(null);
37403
+ const allowedFor = Object.create(null);
37404
+ for (const server of connected || []) {
37405
+ const name = server.serverName;
37406
+ if (!wanted.has(name) || !inBucket(server, workspaceId)) continue;
37407
+ if (providerLimits && !(name in providerLimits)) continue; // fail closed
37408
+ const allowed = effectiveAllowedTools(
37409
+ providerLimits ? providerLimits[name] : undefined,
37410
+ selections[name],
37411
+ );
37412
+ allowedFor[name] = allowed;
37413
+ for (const tool of server.tools || []) {
37414
+ if (allowed && !allowed.includes(tool.name)) continue;
37415
+ tools.push(tool);
37416
+ toolServer[tool.name] = name;
37417
+ }
37418
+ }
37419
+ return { tools, toolServer, allowedFor };
37420
+ }
37421
+
37422
+ /**
37423
+ * Last-line enforcement for a bot tool call. `allowedMap` is the
37424
+ * `allowedFor` from this bot's resolveBotTools. Fails closed: unknown bot,
37425
+ * unknown provider, or a tool outside the whitelist → rejected.
37426
+ *
37427
+ * @returns {{ ok: true, allowed: string[]|null } | { ok: false, message: string }}
37428
+ */
37429
+ function checkToolCall$1(allowedMap, serverName, toolName) {
37430
+ if (
37431
+ !allowedMap ||
37432
+ !Object.prototype.hasOwnProperty.call(allowedMap, serverName)
37433
+ ) {
37434
+ return {
37435
+ ok: false,
37436
+ message: `Tool "${toolName}" is not available to this bot (provider "${serverName}" isn't one of its providers).`,
37437
+ };
37438
+ }
37439
+ const allowed = allowedMap[serverName];
37440
+ if (Array.isArray(allowed) && !allowed.includes(toolName)) {
37441
+ return {
37442
+ ok: false,
37443
+ message: `Tool "${toolName}" is not allowed for this bot on "${serverName}".`,
37444
+ };
37445
+ }
37446
+ return { ok: true, allowed: Array.isArray(allowed) ? allowed : null };
37447
+ }
37448
+
37449
+ var toolSources = {
37450
+ checkToolCall: checkToolCall$1,
37451
+ connectedServersFromMap: connectedServersFromMap$1,
37452
+ listToolSources: listToolSources$1,
37453
+ ensureBotServers: ensureBotServers$1,
37454
+ resolveBotTools: resolveBotTools$1,
37455
+ effectiveAllowedTools,
37456
+ };
37457
+
37202
37458
  /**
37203
37459
  * mcpScopeResolver.js
37204
37460
  *
@@ -37577,6 +37833,7 @@ const os$2 = require$$2;
37577
37833
  const responseCache$2 = responseCache_1;
37578
37834
  const { gateToolCall, gateToolCallWithJit } = permissionGate;
37579
37835
  const { serverKey, parseServerKey } = mcpServerKey;
37836
+ const { connectedServersFromMap } = toolSources;
37580
37837
  const { applyPathScopeToCredentials } = mcpScopeResolver;
37581
37838
  const { readEnforceFlag: readEnforceFlag$1, readJitFlag: readJitFlag$1 } = securityFlags;
37582
37839
  const { app: app$8 } = require$$0$2;
@@ -37750,7 +38007,7 @@ function getShellPath$1() {
37750
38007
  return _shellPath$1;
37751
38008
  }
37752
38009
 
37753
- const { execSync } = require$$12;
38010
+ const { execSync } = require$$13;
37754
38011
  const fallbackDirs = ["/usr/local/bin", "/opt/homebrew/bin"];
37755
38012
 
37756
38013
  // Scan nvm versions, tracking both latest and best compatible version
@@ -38854,24 +39111,16 @@ const mcpController$3 = {
38854
39111
  /**
38855
39112
  * listConnectedServers
38856
39113
  * Returns all connected servers with their cached tool lists.
38857
- * Used by llmController to discover available MCP tools.
39114
+ * Used by bots + the LLM tool picker to discover available MCP tools.
39115
+ *
39116
+ * `serverName` is the real server name and `workspaceId` its bucket
39117
+ * (null = no workspace) — activeServers is keyed "<workspace>::<server>",
39118
+ * and returning that compound key as `serverName` broke name matching.
38858
39119
  *
38859
- * @returns {Array<{ serverName, tools, resources, status }>}
39120
+ * @returns {Array<{ serverName, workspaceId, tools, resources, status }>}
38860
39121
  */
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
- },
39122
+ listConnectedServers: () =>
39123
+ connectedServersFromMap(activeServers, STATUS$1.CONNECTED),
38875
39124
 
38876
39125
  /**
38877
39126
  * runAuth
@@ -38884,7 +39133,7 @@ const mcpController$3 = {
38884
39133
  * @returns {{ success } | { error, message }}
38885
39134
  */
38886
39135
  runAuth: async (win, mcpConfig, credentials, authCommand) => {
38887
- const { spawn } = require$$12;
39136
+ const { spawn } = require$$13;
38888
39137
 
38889
39138
  const env = cleanEnvForChildProcess();
38890
39139
 
@@ -59749,7 +59998,7 @@ var mcpDashServerController_1 = mcpDashServerController$4;
59749
59998
  * can use the Chat widget without a separate API key.
59750
59999
  */
59751
60000
 
59752
- const { spawn, execSync } = require$$12;
60001
+ const { spawn, execSync } = require$$13;
59753
60002
  const {
59754
60003
  LLM_STREAM_DELTA: LLM_STREAM_DELTA$2,
59755
60004
  LLM_STREAM_TOOL_CALL: LLM_STREAM_TOOL_CALL$2,
@@ -73788,6 +74037,10 @@ function withDefaults$1(def = {}) {
73788
74037
  // specific engine id here (e.g. "claude-agent").
73789
74038
  engine: null,
73790
74039
  mcpServers: [],
74040
+ // Per-provider narrowing within each provider's declared tools:
74041
+ // { [providerName]: string[] }. No entry → every tool the provider allows.
74042
+ toolSelections: {},
74043
+ // Tools that skip the approval prompt (NOT the tools the bot may use).
73791
74044
  allowedTools: [],
73792
74045
  approvalPolicy: "ask",
73793
74046
  schedules: [],
@@ -73849,6 +74102,22 @@ function validateBotDefinition$1(def) {
73849
74102
  errors.push(`${field} must be an array`);
73850
74103
  }
73851
74104
  }
74105
+ // toolSelections: { [providerName]: string[] } — per-bot tool narrowing.
74106
+ if (def.toolSelections !== undefined) {
74107
+ const ts = def.toolSelections;
74108
+ const ok =
74109
+ ts &&
74110
+ typeof ts === "object" &&
74111
+ !Array.isArray(ts) &&
74112
+ Object.values(ts).every(
74113
+ (v) => Array.isArray(v) && v.every((t) => typeof t === "string"),
74114
+ );
74115
+ if (!ok) {
74116
+ errors.push(
74117
+ "toolSelections must map provider names to arrays of tool names",
74118
+ );
74119
+ }
74120
+ }
73852
74121
  // Each subscription must name an eventType (PRD FR-009).
73853
74122
  if (Array.isArray(def.subscriptions)) {
73854
74123
  for (const sub of def.subscriptions) {
@@ -74998,18 +75267,29 @@ function _makeCanUseTool(ctx) {
74998
75267
  async function _runAgent(ctx, stream) {
74999
75268
  const query = await _loadQuery();
75000
75269
 
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.
75270
+ // Per-run environment for the agent process. Auth: the Claude Code login by
75271
+ // default; a bot's Anthropic API key, when set, goes in THIS run's env only —
75272
+ // never process.env, which would leak it to the rest of the main process and
75273
+ // to other bots.
75274
+ const env = { ...process.env };
75004
75275
  if (ctx.credentials && ctx.credentials.apiKey) {
75005
- process.env.ANTHROPIC_API_KEY = ctx.credentials.apiKey;
75276
+ env.ANTHROPIC_API_KEY = ctx.credentials.apiKey;
75006
75277
  }
75278
+ // No claude.ai connectors from the user's Claude login.
75279
+ env.ENABLE_CLAUDEAI_MCP_SERVERS = "false";
75007
75280
 
75008
75281
  const options = {
75009
75282
  model: ctx.model,
75010
75283
  systemPrompt: ctx.systemPrompt || { type: "preset", preset: "claude_code" },
75011
75284
  permissionMode: "default",
75012
75285
  canUseTool: _makeCanUseTool(ctx),
75286
+ // Isolation: a bot sees only the SDK's built-in tools + the Dash providers
75287
+ // it was granted (bridged below as "bot-mcp"). Don't load the user's
75288
+ // ~/.claude settings/plugins/skills/hooks, and ignore every MCP config
75289
+ // other than the servers passed in `mcpServers`.
75290
+ settingSources: [],
75291
+ strictMcpConfig: true,
75292
+ env,
75013
75293
  };
75014
75294
  if (ctx.maxTurns) options.maxTurns = ctx.maxTurns;
75015
75295
  if (ctx.workingDir) options.cwd = ctx.workingDir;
@@ -75445,12 +75725,17 @@ let BotRunner$1 = class BotRunner {
75445
75725
  }
75446
75726
  return this._callTool(serverName, toolName, input, {
75447
75727
  workspaceId: bot.workspaceId,
75728
+ // Lets the caller apply this bot's per-provider tool limits.
75729
+ botId,
75448
75730
  });
75449
75731
  };
75450
75732
 
75451
- // Resume only when the stored session was produced by this engine.
75733
+ // A run does the bot's job against current data → fresh conversation.
75734
+ // Resume only when explicitly continuing (reply-to-continue) AND the
75735
+ // stored session was produced by this engine. Anything a bot should
75736
+ // carry between runs belongs in its memory tools.
75452
75737
  const session =
75453
- bot.session && bot.session.engine === engine.id
75738
+ opts.continueSession && bot.session && bot.session.engine === engine.id
75454
75739
  ? bot.session.state
75455
75740
  : null;
75456
75741
 
@@ -76284,6 +76569,12 @@ const {
76284
76569
  const { matchSubscribedBots } = eventMatcher;
76285
76570
  const { EventDispatcher } = EventDispatcher_1;
76286
76571
  const { normalizeMcpResult } = mcpResult;
76572
+ const {
76573
+ listToolSources,
76574
+ ensureBotServers,
76575
+ resolveBotTools,
76576
+ checkToolCall,
76577
+ } = toolSources;
76287
76578
  const {
76288
76579
  getProvider: getProvider$1,
76289
76580
  getDefaultModel,
@@ -76387,6 +76678,10 @@ const botController$1 = {
76387
76678
  // flood the runner with event-triggered runs.
76388
76679
  this._dispatcher = new EventDispatcher();
76389
76680
 
76681
+ // botId → { [providerName]: allowed tool names | null }, set on each run's
76682
+ // tool resolution and enforced in _callTool.
76683
+ this._allowedByBot = new Map();
76684
+
76390
76685
  this._runner = new BotRunner({
76391
76686
  engines,
76392
76687
  store: this._store,
@@ -76472,6 +76767,26 @@ const botController$1 = {
76472
76767
  return this._store.list();
76473
76768
  },
76474
76769
 
76770
+ /**
76771
+ * The user's Dash MCP providers a bot can use (Settings → Providers), with
76772
+ * running status + tool count. Names/types only — never credentials.
76773
+ */
76774
+ listToolSources(workspaceId = null) {
76775
+ let providers = [];
76776
+ try {
76777
+ const win = this._getMainWindow();
76778
+ ({ providers = [] } =
76779
+ this._providers.listProviders(win, this._appId) || {});
76780
+ } catch (_e) {
76781
+ providers = [];
76782
+ }
76783
+ return listToolSources({
76784
+ providers,
76785
+ connected: this._connectedServers(),
76786
+ workspaceId,
76787
+ });
76788
+ },
76789
+
76475
76790
  get(botId) {
76476
76791
  return this._store.get(botId);
76477
76792
  },
@@ -76638,23 +76953,58 @@ const botController$1 = {
76638
76953
  };
76639
76954
  },
76640
76955
 
76641
- /** Build the bot's tool set + tool→server resolver from connected servers. */
76642
- _resolveTools(bot) {
76643
- const connected = this._mcp.listConnectedServers
76956
+ /** Connected MCP servers with real server names + workspace buckets. */
76957
+ _connectedServers() {
76958
+ return this._mcp && this._mcp.listConnectedServers
76644
76959
  ? this._mcp.listConnectedServers()
76645
76960
  : [];
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;
76961
+ },
76962
+
76963
+ /**
76964
+ * Build the bot's tool set + tool→server resolver. First starts any of the
76965
+ * bot's selected providers that aren't running in its bucket — through the
76966
+ * same deduping startServer factory the dashboards use — so a scheduled or
76967
+ * event-triggered bot doesn't silently run without its tools. Start
76968
+ * failures are surfaced in the run feed as warnings.
76969
+ */
76970
+ async _resolveTools(bot) {
76971
+ if (this._mcp && this._mcp.startServer && this._providers) {
76972
+ const win = this._getMainWindow();
76973
+ const { failed } = await ensureBotServers({
76974
+ bot,
76975
+ connected: this._connectedServers(),
76976
+ getProvider: async (name) =>
76977
+ this._providers.getProvider(win, this._appId, name),
76978
+ startServer: (name, mcpConfig, credentials, workspaceId) =>
76979
+ this._mcp.startServer(
76980
+ win,
76981
+ name,
76982
+ mcpConfig,
76983
+ credentials,
76984
+ workspaceId,
76985
+ null,
76986
+ this._appId,
76987
+ ),
76988
+ });
76989
+ for (const f of failed) {
76990
+ this._broadcast(BOT_STREAM$1, {
76991
+ botId: bot.id,
76992
+ event: {
76993
+ type: "warning",
76994
+ message: `${f.serverName} couldn't start, so its tools aren't available this run: ${f.message}`,
76995
+ },
76996
+ });
76656
76997
  }
76657
76998
  }
76999
+ // Each provider's declared tools (Settings → Providers) are the ceiling;
77000
+ // the bot's toolSelections narrow within them.
77001
+ const { tools, toolServer, allowedFor } = resolveBotTools({
77002
+ bot,
77003
+ connected: this._connectedServers(),
77004
+ providerLimits: this._providerToolLimits(),
77005
+ });
77006
+ // Remembered per bot so _callTool can hard-enforce the same whitelist.
77007
+ this._allowedByBot.set(bot.id, allowedFor);
76658
77008
  // Always-available in-process memory tools (P1: FR-010).
76659
77009
  for (const tool of MEMORY_TOOLS) {
76660
77010
  tools.push(tool);
@@ -76674,20 +77024,54 @@ const botController$1 = {
76674
77024
  );
76675
77025
  }
76676
77026
  const win = this._getMainWindow();
77027
+ // The bot's per-provider whitelist (provider limit ∩ bot selection), from
77028
+ // the last _resolveTools for this bot. A tool outside it is rejected —
77029
+ // mirroring the allowedTools enforcement widgets get. If no entry exists
77030
+ // for this provider, the bot never had it resolved → deny.
77031
+ const check = checkToolCall(
77032
+ this._allowedByBot.get(opts.botId),
77033
+ serverName,
77034
+ toolName,
77035
+ );
77036
+ if (!check.ok) return { text: check.message, isError: true };
77037
+ const { allowed } = check;
76677
77038
  // widgetId + token are null → the widget permission gate is bypassed; bot
76678
- // tool calls are gated upstream by the bot PermissionGate.
77039
+ // tool calls are gated upstream by the bot PermissionGate. `allowed` is
77040
+ // also passed as callTool's whitelist (defense in depth).
76679
77041
  const result = await this._mcp.callTool(
76680
77042
  win,
76681
77043
  serverName,
76682
77044
  toolName,
76683
77045
  args,
76684
- null,
77046
+ allowed || null,
76685
77047
  null,
76686
77048
  opts.workspaceId || null,
76687
77049
  null,
76688
77050
  );
76689
77051
  return normalizeMcpResult(result);
76690
77052
  },
77053
+
77054
+ /** { [providerName]: allowedTools | null } for the user's MCP providers. */
77055
+ _providerToolLimits() {
77056
+ const limits = Object.create(null);
77057
+ try {
77058
+ const win = this._getMainWindow();
77059
+ const { providers = [] } =
77060
+ this._providers.listProviders(win, this._appId) || {};
77061
+ for (const p of providers) {
77062
+ if (p && p.providerClass === "mcp" && p.name) {
77063
+ limits[p.name] = Array.isArray(p.allowedTools)
77064
+ ? p.allowedTools
77065
+ : null;
77066
+ }
77067
+ }
77068
+ } catch (_e) {
77069
+ // Provider info unreadable → empty map. resolveBotTools treats a
77070
+ // provider with no entry as not configured, so the bot gets NO tools
77071
+ // from it (fails closed) rather than every tool the server exposes.
77072
+ }
77073
+ return limits;
77074
+ },
76691
77075
  };
76692
77076
 
76693
77077
  var botController_1 = botController$1;
@@ -80600,6 +80984,7 @@ const {
80600
80984
  BOTS_PAUSE_BOT,
80601
80985
  BOTS_RESUME_BOT,
80602
80986
  BOTS_GET_PAUSE_STATE,
80987
+ BOTS_LIST_TOOL_SOURCES,
80603
80988
  BOT_STREAM,
80604
80989
  BOT_APPROVAL_PENDING,
80605
80990
  BOT_BUDGET_ALERT,
@@ -80648,6 +81033,10 @@ const botApi$1 = {
80648
81033
  resumeBot: (botId) => ipcRenderer$3.invoke(BOTS_RESUME_BOT, { botId }),
80649
81034
  getPauseState: () => ipcRenderer$3.invoke(BOTS_GET_PAUSE_STATE),
80650
81035
 
81036
+ /** The user's Dash MCP providers a bot can use: [{ name, type, running, toolCount }]. */
81037
+ listToolSources: (workspaceId = null) =>
81038
+ ipcRenderer$3.invoke(BOTS_LIST_TOOL_SOURCES, { workspaceId }),
81039
+
80651
81040
  /** Subscribe to streamed run events: { botId, event }. */
80652
81041
  onStream: (callback) => _addListener(BOT_STREAM, callback),
80653
81042
  /** Subscribe to new pending approvals: { id, request }. */