@trops/dash-core 0.1.638 → 0.1.640

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.
@@ -983,6 +983,8 @@ 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
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";
986
988
 
987
989
  // --- Main → Renderer (send) ---
988
990
  const BOT_STREAM$2 = "bot-stream"; // { botId, event: BotEvent }
@@ -1010,6 +1012,8 @@ var botEvents$1 = {
1010
1012
  BOTS_RESUME_BOT: BOTS_RESUME_BOT$1,
1011
1013
  BOTS_GET_PAUSE_STATE: BOTS_GET_PAUSE_STATE$1,
1012
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,
1013
1017
  BOT_STREAM: BOT_STREAM$2,
1014
1018
  BOT_APPROVAL_PENDING: BOT_APPROVAL_PENDING$2,
1015
1019
  BOT_BUDGET_ALERT: BOT_BUDGET_ALERT$2,
@@ -19492,7 +19496,7 @@ util$E.isObject = isObject;
19492
19496
  util$E.isPlainObject = isPlainObject;
19493
19497
  util$E.numKeys = numKeys;
19494
19498
  util$E.escapeRegex = escapeRegex;
19495
- util$E.clone = clone;
19499
+ util$E.clone = clone$1;
19496
19500
  util$E.normalizeParams = normalizeParams;
19497
19501
  util$E.createTransparentProxy = createTransparentProxy;
19498
19502
  util$E.stringifyPrimitive = stringifyPrimitive;
@@ -19715,7 +19719,7 @@ function escapeRegex(str) {
19715
19719
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19716
19720
  }
19717
19721
  // zod-specific utils
19718
- function clone(inst, def, params) {
19722
+ function clone$1(inst, def, params) {
19719
19723
  const cl = new inst._zod.constr(def ?? inst._zod.def);
19720
19724
  if (!def || params?.parent)
19721
19725
  cl._zod.parent = inst;
@@ -19805,7 +19809,7 @@ function pick$1(schema, mask) {
19805
19809
  // pick key
19806
19810
  newShape[key] = currDef.shape[key];
19807
19811
  }
19808
- return clone(schema, {
19812
+ return clone$1(schema, {
19809
19813
  ...schema._zod.def,
19810
19814
  shape: newShape,
19811
19815
  checks: [],
@@ -19822,7 +19826,7 @@ function omit(schema, mask) {
19822
19826
  continue;
19823
19827
  delete newShape[key];
19824
19828
  }
19825
- return clone(schema, {
19829
+ return clone$1(schema, {
19826
19830
  ...schema._zod.def,
19827
19831
  shape: newShape,
19828
19832
  checks: [],
@@ -19841,10 +19845,10 @@ function extend(schema, shape) {
19841
19845
  },
19842
19846
  checks: [], // delete existing checks
19843
19847
  };
19844
- return clone(schema, def);
19848
+ return clone$1(schema, def);
19845
19849
  }
19846
19850
  function merge(a, b) {
19847
- return clone(a, {
19851
+ return clone$1(a, {
19848
19852
  ...a._zod.def,
19849
19853
  get shape() {
19850
19854
  const _shape = { ...a._zod.def.shape, ...b._zod.def.shape };
@@ -19885,7 +19889,7 @@ function partial(Class, schema, mask) {
19885
19889
  : oldShape[key];
19886
19890
  }
19887
19891
  }
19888
- return clone(schema, {
19892
+ return clone$1(schema, {
19889
19893
  ...schema._zod.def,
19890
19894
  shape,
19891
19895
  checks: [],
@@ -19917,7 +19921,7 @@ function required$3(Class, schema, mask) {
19917
19921
  });
19918
19922
  }
19919
19923
  }
19920
- return clone(schema, {
19924
+ return clone$1(schema, {
19921
19925
  ...schema._zod.def,
19922
19926
  shape,
19923
19927
  // optional: [],
@@ -36674,7 +36678,7 @@ function _resolveIdentity({ token, widgetId }) {
36674
36678
  // Argument keys that look like paths. Different MCP servers use
36675
36679
  // different conventions; this list covers the common filesystem-style
36676
36680
  // servers. Extensible — add as new patterns surface.
36677
- const PATH_ARG_KEYS = ["path", "uri", "filepath", "file", "directory"];
36681
+ const PATH_ARG_KEYS$1 = ["path", "uri", "filepath", "file", "directory"];
36678
36682
 
36679
36683
  // Heuristic: tool names matching this regex are treated as writes for
36680
36684
  // purposes of choosing readPaths vs writePaths. The match is intentionally
@@ -36683,7 +36687,7 @@ const PATH_ARG_KEYS = ["path", "uri", "filepath", "file", "directory"];
36683
36687
  const WRITE_TOOL_PATTERN =
36684
36688
  /(^|_)(write|create|edit|delete|remove|append|move|rename|chmod|chown|mkdir)/i;
36685
36689
 
36686
- function isWriteTool(toolName) {
36690
+ function isWriteTool$1(toolName) {
36687
36691
  if (typeof toolName !== "string") return false;
36688
36692
  return WRITE_TOOL_PATTERN.test(toolName);
36689
36693
  }
@@ -36757,7 +36761,7 @@ function _evaluateGrant({
36757
36761
 
36758
36762
  // Path-argument containment. Only checked when the tool's args
36759
36763
  // include a path-shaped key.
36760
- const isWrite = isWriteTool(toolName);
36764
+ const isWrite = isWriteTool$1(toolName);
36761
36765
  // Write tools must use writePaths; read tools may use either
36762
36766
  // readPaths or writePaths (write access implies read access).
36763
36767
  const allowedPaths = isWrite
@@ -36765,7 +36769,7 @@ function _evaluateGrant({
36765
36769
  : [...serverPerms.readPaths, ...serverPerms.writePaths];
36766
36770
 
36767
36771
  if (args && typeof args === "object") {
36768
- for (const key of PATH_ARG_KEYS) {
36772
+ for (const key of PATH_ARG_KEYS$1) {
36769
36773
  const v = args[key];
36770
36774
  if (typeof v !== "string" || !v) continue;
36771
36775
  if (allowedPaths.length === 0) {
@@ -37134,8 +37138,8 @@ var permissionGate = {
37134
37138
  gateToolCall: gateToolCall$1,
37135
37139
  gateBotToolCall,
37136
37140
  gateToolCallWithJit: gateToolCallWithJit$1,
37137
- isWriteTool,
37138
- PATH_ARG_KEYS,
37141
+ isWriteTool: isWriteTool$1,
37142
+ PATH_ARG_KEYS: PATH_ARG_KEYS$1,
37139
37143
  };
37140
37144
 
37141
37145
  /**
@@ -75184,6 +75188,9 @@ var agentToolBridge = {
75184
75188
  const { createEventStream } = eventStream;
75185
75189
  const { BOT_MCP_SERVER, buildBotMcpServer } = agentToolBridge;
75186
75190
 
75191
+ // The SDK names bridged tools "mcp__<server>__<tool>".
75192
+ const BRIDGED_PREFIX = `mcp__${BOT_MCP_SERVER}__`;
75193
+
75187
75194
  // Test seam: injected fake `query` implementation (see __setQueryForTest).
75188
75195
  let _queryImpl = null;
75189
75196
 
@@ -75243,7 +75250,27 @@ function _makeCanUseTool(ctx) {
75243
75250
  if (ctx.approvalPolicy === "allow") {
75244
75251
  return { behavior: "allow", updatedInput: input };
75245
75252
  }
75246
- // 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.
75247
75274
  if (typeof ctx.createApproval !== "function") {
75248
75275
  return { behavior: "deny", message: "no approval channel available" };
75249
75276
  }
@@ -76422,6 +76449,19 @@ var memoryTools = { MEMORY_SERVER: MEMORY_SERVER$1, MEMORY_TOOLS: MEMORY_TOOLS$1
76422
76449
  * Pure (NFR-006): no Electron, no I/O.
76423
76450
  */
76424
76451
 
76452
+ /**
76453
+ * Copied dashboards reuse widget ids, so one eventType can come from several
76454
+ * dashboards. A subscription picked in the bot form records its dashboard
76455
+ * (`source.workspaceId`); it then only matches events published from that
76456
+ * dashboard. Unstamped events and dashboard-less subscriptions still match.
76457
+ */
76458
+ function sameDashboard(sub, event) {
76459
+ const want = sub.source && sub.source.workspaceId;
76460
+ const got = event.workspaceId;
76461
+ if (want == null || want === "" || got == null || got === "") return true;
76462
+ return String(want) === String(got);
76463
+ }
76464
+
76425
76465
  /**
76426
76466
  * @param {Array<object>} bots bot definitions (each may have `subscriptions`)
76427
76467
  * @param {{ eventType: string, workspaceId?: string }} event
@@ -76436,7 +76476,9 @@ function matchSubscribedBots$1(bots, event, opts = {}) {
76436
76476
  return bots.filter((bot) => {
76437
76477
  if (!bot || bot.id === excludeBotId) return false;
76438
76478
  const subs = Array.isArray(bot.subscriptions) ? bot.subscriptions : [];
76439
- const subscribed = subs.some((s) => s && s.eventType === event.eventType);
76479
+ const subscribed = subs.some(
76480
+ (s) => s && s.eventType === event.eventType && sameDashboard(s, event),
76481
+ );
76440
76482
  if (!subscribed) return false;
76441
76483
 
76442
76484
  // Workspace scoping (forward-compatible): only enforced once events carry a
@@ -76538,6 +76580,111 @@ function normalizeMcpResult$1(mcpResult) {
76538
76580
 
76539
76581
  var mcpResult = { normalizeMcpResult: normalizeMcpResult$1 };
76540
76582
 
76583
+ /**
76584
+ * rememberGrant.js — turn an "Always allow" bot approval into a durable grant,
76585
+ * and revoke it.
76586
+ *
76587
+ * Bot grants live in the shared grant store (electron/mcp/grantedPermissions)
76588
+ * keyed by botId, shape `{ servers: { [provider]: { tools, readPaths,
76589
+ * writePaths } } }`, and are enforced by the bot permission gate
76590
+ * (gateBotToolCall). A remembered approval is scoped to this bot + this
76591
+ * provider + this tool, plus — when the tool call carries a path argument —
76592
+ * the folder of that path (write folder for write tools, read folder
76593
+ * otherwise). Nothing broader.
76594
+ *
76595
+ * Pure (NFR-006): the gate's path rules (isWriteTool, path arg keys) and
76596
+ * `dirname` are injected so this matches the real gate exactly.
76597
+ */
76598
+
76599
+ const LISTING_TOOL = /(list|director|tree|search)/i;
76600
+
76601
+ /**
76602
+ * The folder to remember for a path argument: a `directory` arg, or a
76603
+ * directory-listing tool, names the folder itself; any other path is a file,
76604
+ * so its parent folder is remembered.
76605
+ */
76606
+ function folderForPathArg(key, value, toolName, dirname) {
76607
+ if (key === "directory" || LISTING_TOOL.test(String(toolName || ""))) {
76608
+ return value;
76609
+ }
76610
+ return dirname(value);
76611
+ }
76612
+
76613
+ const clone = (perms) =>
76614
+ perms && typeof perms === "object" ? JSON.parse(JSON.stringify(perms)) : {};
76615
+
76616
+ const addUnique = (list, value) =>
76617
+ list.includes(value) ? list : [...list, value];
76618
+
76619
+ /**
76620
+ * @param {object|null} perms the bot's current grant (or null)
76621
+ * @param {{ serverName: string, toolName: string, args?: object }} call
76622
+ * @param {{ isWriteTool: Function, pathArgKeys: string[], dirname: Function }} helpers
76623
+ * @returns {object} the new grant (input is not mutated)
76624
+ */
76625
+ function rememberToolGrant$1(perms, call, helpers) {
76626
+ const { serverName, toolName, args } = call;
76627
+ const next = clone(perms);
76628
+ next.grantOrigin = next.grantOrigin || "manual";
76629
+ next.servers = next.servers || {};
76630
+ const server = next.servers[serverName] || {
76631
+ tools: [],
76632
+ readPaths: [],
76633
+ writePaths: [],
76634
+ };
76635
+ server.tools = addUnique(server.tools || [], toolName);
76636
+ server.readPaths = server.readPaths || [];
76637
+ server.writePaths = server.writePaths || [];
76638
+
76639
+ if (args && typeof args === "object") {
76640
+ const isWrite = helpers.isWriteTool(toolName);
76641
+ for (const key of helpers.pathArgKeys) {
76642
+ const v = args[key];
76643
+ if (typeof v !== "string" || !v) continue;
76644
+ const folder = folderForPathArg(key, v, toolName, helpers.dirname);
76645
+ if (isWrite) server.writePaths = addUnique(server.writePaths, folder);
76646
+ else server.readPaths = addUnique(server.readPaths, folder);
76647
+ }
76648
+ }
76649
+
76650
+ next.servers[serverName] = server;
76651
+ return next;
76652
+ }
76653
+
76654
+ /**
76655
+ * Revoke one remembered tool. When it was the provider's last remembered
76656
+ * tool, the provider entry (and its folders) is removed entirely.
76657
+ */
76658
+ function forgetToolGrant$1(perms, serverName, toolName) {
76659
+ const server = perms && perms.servers && perms.servers[serverName];
76660
+ if (!server || !(server.tools || []).includes(toolName)) return perms;
76661
+ const next = clone(perms);
76662
+ const tools = next.servers[serverName].tools.filter((t) => t !== toolName);
76663
+ if (tools.length) next.servers[serverName].tools = tools;
76664
+ else delete next.servers[serverName];
76665
+ return next;
76666
+ }
76667
+
76668
+ /** Remembered tools + folders per provider, for display in the bot form. */
76669
+ function summarizeGrants$1(perms) {
76670
+ const out = {};
76671
+ const servers = (perms && perms.servers) || {};
76672
+ for (const [name, s] of Object.entries(servers)) {
76673
+ out[name] = {
76674
+ tools: [...(s.tools || [])],
76675
+ folders: [...(s.readPaths || []), ...(s.writePaths || [])],
76676
+ };
76677
+ }
76678
+ return out;
76679
+ }
76680
+
76681
+ var rememberGrant = {
76682
+ rememberToolGrant: rememberToolGrant$1,
76683
+ forgetToolGrant: forgetToolGrant$1,
76684
+ summarizeGrants: summarizeGrants$1,
76685
+ folderForPathArg,
76686
+ };
76687
+
76541
76688
  /**
76542
76689
  * botController.js
76543
76690
  *
@@ -76575,6 +76722,13 @@ const {
76575
76722
  resolveBotTools,
76576
76723
  checkToolCall,
76577
76724
  } = toolSources;
76725
+ const {
76726
+ rememberToolGrant,
76727
+ forgetToolGrant,
76728
+ summarizeGrants,
76729
+ } = rememberGrant;
76730
+ const grantStore = grantedPermissions;
76731
+ const { isWriteTool, PATH_ARG_KEYS } = permissionGate;
76578
76732
  const {
76579
76733
  getProvider: getProvider$1,
76580
76734
  getDefaultModel,
@@ -76802,6 +76956,8 @@ const botController$1 = {
76802
76956
 
76803
76957
  delete(botId) {
76804
76958
  this._scheduler.unregister(botId);
76959
+ // A deleted bot's remembered approvals go with it.
76960
+ grantStore.revokeGrant(botId);
76805
76961
  return this._store.delete(botId);
76806
76962
  },
76807
76963
 
@@ -76813,10 +76969,50 @@ const botController$1 = {
76813
76969
  return { stopped: this._runner.abort(botId) };
76814
76970
  },
76815
76971
 
76972
+ /**
76973
+ * Resolve a pending approval. `decision.remember` ("Always allow") also
76974
+ * saves a durable grant for this bot + provider + tool (+ the folder of a
76975
+ * path argument), so later runs use that tool without asking. Only provider
76976
+ * tool approvals can be remembered — built-in agent tools have no provider
76977
+ * and always ask.
76978
+ */
76816
76979
  approve(approvalId, decision = {}) {
76817
- return decision.allow
76818
- ? { ok: this._approvals.resolve(approvalId, decision) }
76819
- : { ok: this._approvals.deny(approvalId, decision.reason) };
76980
+ if (!decision.allow) {
76981
+ return { ok: this._approvals.deny(approvalId, decision.reason) };
76982
+ }
76983
+ let remembered = false;
76984
+ if (decision.remember) {
76985
+ const pending = this._approvals.get(approvalId);
76986
+ const req = pending && pending.request;
76987
+ if (req && req.botId && req.serverName && req.toolName) {
76988
+ const next = rememberToolGrant(
76989
+ grantStore.getGrant(req.botId),
76990
+ {
76991
+ serverName: req.serverName,
76992
+ toolName: req.toolName,
76993
+ args: req.input,
76994
+ },
76995
+ { isWriteTool, pathArgKeys: PATH_ARG_KEYS, dirname: path$2.dirname },
76996
+ );
76997
+ remembered = grantStore.setGrant(req.botId, next) === true;
76998
+ }
76999
+ }
77000
+ return { ok: this._approvals.resolve(approvalId, decision), remembered };
77001
+ },
77002
+
77003
+ /** Remembered approvals for a bot: { [provider]: { tools, folders } }. */
77004
+ getGrants(botId) {
77005
+ return summarizeGrants(grantStore.getGrant(botId));
77006
+ },
77007
+
77008
+ /** Revoke one remembered tool approval; returns the updated summary. */
77009
+ revokeGrant(botId, serverName, toolName) {
77010
+ const current = grantStore.getGrant(botId);
77011
+ if (current) {
77012
+ const next = forgetToolGrant(current, serverName, toolName);
77013
+ if (next !== current) grantStore.setGrant(botId, next);
77014
+ }
77015
+ return this.getGrants(botId);
76820
77016
  },
76821
77017
 
76822
77018
  listApprovals() {
@@ -80985,6 +81181,8 @@ const {
80985
81181
  BOTS_RESUME_BOT,
80986
81182
  BOTS_GET_PAUSE_STATE,
80987
81183
  BOTS_LIST_TOOL_SOURCES,
81184
+ BOTS_GET_GRANTS,
81185
+ BOTS_REVOKE_GRANT,
80988
81186
  BOT_STREAM,
80989
81187
  BOT_APPROVAL_PENDING,
80990
81188
  BOT_BUDGET_ALERT,
@@ -81037,6 +81235,12 @@ const botApi$1 = {
81037
81235
  listToolSources: (workspaceId = null) =>
81038
81236
  ipcRenderer$3.invoke(BOTS_LIST_TOOL_SOURCES, { workspaceId }),
81039
81237
 
81238
+ /** Remembered approvals ("Always allow"): { [provider]: { tools, folders } }. */
81239
+ getGrants: (botId) => ipcRenderer$3.invoke(BOTS_GET_GRANTS, { botId }),
81240
+ /** Revoke one remembered tool approval; resolves to the updated summary. */
81241
+ revokeGrant: (botId, serverName, toolName) =>
81242
+ ipcRenderer$3.invoke(BOTS_REVOKE_GRANT, { botId, serverName, toolName }),
81243
+
81040
81244
  /** Subscribe to streamed run events: { botId, event }. */
81041
81245
  onStream: (callback) => _addListener(BOT_STREAM, callback),
81042
81246
  /** Subscribe to new pending approvals: { id, request }. */
@@ -82391,8 +82595,15 @@ function createMainApi$1(extensions = {}) {
82391
82595
  mcpDashServer: mcpDashServerApi$1,
82392
82596
 
82393
82597
  widgetEvent: {
82394
- publish: (eventType, content) => {
82395
- ipcRenderer.send("widget-event:publish", { eventType, content });
82598
+ // meta.workspaceId = the publishing dashboard (bot subscriptions use it).
82599
+ publish: (eventType, content, meta) => {
82600
+ const workspaceId = meta && meta.workspaceId;
82601
+ ipcRenderer.send(
82602
+ "widget-event:publish",
82603
+ workspaceId
82604
+ ? { eventType, content, workspaceId }
82605
+ : { eventType, content },
82606
+ );
82396
82607
  },
82397
82608
  getLastEvents: () => ipcRenderer.invoke("widget-event:get-last-events"),
82398
82609
  },