@mrciphersmith/keryx 0.3.34 → 0.3.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -740,7 +740,10 @@ advertised surface stays a fixed cost instead of growing with your server
740
740
  list. Every call goes through the same approval prompt as `shell_exec`, a
741
741
  server is spawned without keryx's own credentials in its environment, and
742
742
  `--scope project` writes a `.keryx/mcp-servers.json` you can commit while
743
- `keryx mcp disable` stays personal to you.
743
+ `keryx mcp disable` stays personal to you. In `--trust` mode you can trust one
744
+ exact tool for the session (never one its server marks `destructiveHint`);
745
+ `/mcp trust list` and `/mcp trust revoke <server__tool>|all` show and remove
746
+ those grants.
744
747
 
745
748
  Remote servers work the same way — `keryx mcp add linear --transport http
746
749
  <url> --header 'Authorization: Bearer ${LINEAR_TOKEN}'` — and if that
package/dist/cli.js CHANGED
@@ -13447,7 +13447,7 @@ var package_default;
13447
13447
  var init_package = __esm(() => {
13448
13448
  package_default = {
13449
13449
  name: "@mrciphersmith/keryx",
13450
- version: "0.3.34",
13450
+ version: "0.3.35",
13451
13451
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
13452
13452
  private: false,
13453
13453
  publishConfig: {
@@ -71950,7 +71950,7 @@ ${shown.join(`
71950
71950
  result = precomputedResult;
71951
71951
  } else {
71952
71952
  try {
71953
- result = await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.readOnly, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls, signal, deps.busLeases, deps.hardDeny === undefined ? undefined : { check: deps.hardDeny, onDenied: io.onUnattendedDenial }, deps.hooks, untrustedOrigin, io.trustedMcpTools, io.mcpToolFingerprint);
71953
+ result = await executeCall(call, toolByName, io.requestApproval, io.permissionMode, io.readOnly, io.onAutoApproved, hasInvocationCapacity, reserveInvocation, invocationBudget.maxCalls, signal, deps.busLeases, deps.hardDeny === undefined ? undefined : { check: deps.hardDeny, onDenied: io.onUnattendedDenial }, deps.hooks, untrustedOrigin, io.trustedMcpTools, io.mcpToolFingerprint, io.mcpToolDestructive);
71954
71954
  } catch (err) {
71955
71955
  const message = err instanceof Error ? err.message : String(err);
71956
71956
  result = { output: `${call.name} failed: ${message}`, isError: true };
@@ -72520,7 +72520,7 @@ function composeWithHook(toolName, rawDecision, hookResult, interactive) {
72520
72520
  }
72521
72521
  return { decision, hookTightened: true, hookAsked, ...denyMessage !== undefined ? { denyMessage } : {} };
72522
72522
  }
72523
- async function executeCall(call, toolByName, requestApproval, permissionMode, readOnly, onAutoApproved, hasInvocationCapacity, reserveInvocation, maxToolCalls, signal, busLeases, hardDeny, hooks, untrustedOrigin = false, trustedMcpTools, mcpToolFingerprint) {
72523
+ async function executeCall(call, toolByName, requestApproval, permissionMode, readOnly, onAutoApproved, hasInvocationCapacity, reserveInvocation, maxToolCalls, signal, busLeases, hardDeny, hooks, untrustedOrigin = false, trustedMcpTools, mcpToolFingerprint, mcpToolDestructive) {
72524
72524
  const tool = toolByName.get(call.name);
72525
72525
  if (tool === undefined) {
72526
72526
  return { output: `unknown tool: ${call.name}`, isError: true };
@@ -72583,11 +72583,14 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
72583
72583
  const gated = composeWithHook(call.name, rawDecision, hookResult, hookInteractive);
72584
72584
  const mcpFqn = call.name === "use_tool" && typeof input.tool_name === "string" ? input.tool_name : undefined;
72585
72585
  const currentMcpFingerprint = mcpFqn !== undefined ? mcpToolFingerprint?.(mcpFqn) : undefined;
72586
- if (mcpFqn !== undefined && trustedMcpTools?.has(mcpFqn) === true && trustedMcpTools.get(mcpFqn) !== currentMcpFingerprint) {
72586
+ const mcpDestructive = mcpFqn !== undefined && mcpToolDestructive?.(mcpFqn) === true;
72587
+ if (mcpFqn !== undefined && trustedMcpTools?.has(mcpFqn) === true && (mcpDestructive || trustedMcpTools.get(mcpFqn) !== currentMcpFingerprint)) {
72587
72588
  trustedMcpTools.delete(mcpFqn);
72588
72589
  }
72589
- const trustedMcp = mode === "trust" && mcpFqn !== undefined && trustedMcpTools?.has(mcpFqn) === true;
72590
- const mcpTrustEligible = mcpFqn !== undefined && mode === "trust" && !gated.hookAsked && currentMcpFingerprint !== undefined;
72590
+ const mcpGranted = mcpFqn !== undefined && trustedMcpTools?.has(mcpFqn) === true;
72591
+ const trustedMcp = mode === "trust" && mcpGranted;
72592
+ const mcpTrustPossible = mcpFqn !== undefined && mode === "trust" && !gated.hookAsked && currentMcpFingerprint !== undefined;
72593
+ const mcpTrustEligible = mcpTrustPossible && !mcpDestructive;
72591
72594
  if (gated.decision === "deny") {
72592
72595
  return {
72593
72596
  output: gated.hookTightened ? gated.denyMessage : `tool "${call.name}" is not permitted while read-only mode (/plan) is on`,
@@ -72595,7 +72598,7 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
72595
72598
  };
72596
72599
  }
72597
72600
  if (gated.decision === "auto" && !untrustedOrigin || trustedMcp && !untrustedOrigin && !gated.hookAsked && !isReadOnly) {
72598
- onAutoApproved?.(call.name, call.input, { destructive, credentials });
72601
+ onAutoApproved?.(call.name, call.input, { destructive, credentials, ...trustedMcp ? { mcpTrusted: true } : {} });
72599
72602
  } else {
72600
72603
  const fingerprint = toolCallHash(call.name, call.input);
72601
72604
  const response = requestApproval === undefined ? false : await requestApproval(call.name, call.input, {
@@ -72606,7 +72609,8 @@ async function executeCall(call, toolByName, requestApproval, permissionMode, re
72606
72609
  ...publishLease && publishLeaseHolder !== undefined ? { publishLeaseDetail: `held by @${publishLeaseHolder.name} \u2014 "${publishLeaseHolder.reason}"` } : {},
72607
72610
  ...gated.hookAsked ? { hookAsk: true } : {},
72608
72611
  ...untrustedOrigin ? { untrustedOrigin: true } : {},
72609
- ...mcpTrustEligible ? untrustedOrigin ? { mcpTrustWithheld: true } : { mcpTrustAvailable: true } : {}
72612
+ ...mcpTrustPossible ? mcpDestructive ? { mcpTrustWithheld: true, mcpTrustWithheldReason: "destructive" } : untrustedOrigin ? { mcpTrustWithheld: true, mcpTrustWithheldReason: "untrusted-origin" } : { mcpTrustAvailable: true } : {},
72613
+ ...mcpGranted ? { mcpTrusted: true } : {}
72610
72614
  });
72611
72615
  if (!isApprovalFor(response, fingerprint)) {
72612
72616
  return { output: untrustedOrigin ? untrustedDenial : "command not approved by the user; not executed", isError: true };
@@ -81625,7 +81629,7 @@ function renderUseToolApprovalLines(description, meta) {
81625
81629
  const lines = [
81626
81630
  description.title,
81627
81631
  ` server: ${description.server}`,
81628
- ` tool: ${description.tool}`,
81632
+ ` tool: ${description.tool}${meta?.mcpTrusted === true ? ` ${TRUSTED_MARKER}` : ""}`,
81629
81633
  ...description.argumentLines.map((line) => ` ${line}`)
81630
81634
  ];
81631
81635
  if (description.argumentsTruncated) {
@@ -81637,19 +81641,25 @@ function renderUseToolApprovalLines(description, meta) {
81637
81641
  for (const notice of untrustedOriginNotices(meta)) {
81638
81642
  lines.push(` ${notice}`);
81639
81643
  }
81644
+ for (const notice of destructiveTrustNotices(meta)) {
81645
+ lines.push(` ${notice}`);
81646
+ }
81640
81647
  return lines;
81641
81648
  }
81642
81649
  function untrustedOriginNotices(meta) {
81643
81650
  if (meta?.untrustedOrigin !== true)
81644
81651
  return [];
81645
81652
  const notices = ["\u26A0 follows untrusted external content \u2014 that content cannot authorize this call"];
81646
- if (meta.mcpTrustWithheld === true) {
81653
+ if (meta.mcpTrustWithheld === true && meta.mcpTrustWithheldReason !== "destructive") {
81647
81654
  notices.push("trust for this session is not offered: this turn contains external content");
81648
81655
  }
81649
81656
  return notices;
81650
81657
  }
81651
- function mcpTrustOffered(meta, resolvable) {
81652
- return meta?.mcpTrustAvailable === true && meta.untrustedOrigin !== true && meta.fingerprint !== undefined && resolvable;
81658
+ function destructiveTrustNotices(meta) {
81659
+ return meta?.mcpTrustWithheld === true && meta.mcpTrustWithheldReason === "destructive" ? ["trust for this session is not offered: this tool is marked destructive (destructiveHint) by its server"] : [];
81660
+ }
81661
+ function mcpTrustOffered(meta, resolvable, destructive = false) {
81662
+ return meta?.mcpTrustAvailable === true && meta.untrustedOrigin !== true && meta.mcpTrustWithheldReason !== "destructive" && !destructive && meta.fingerprint !== undefined && resolvable;
81653
81663
  }
81654
81664
  function summariseUseToolApproval(description) {
81655
81665
  return `${description.tool} on ${description.server}`;
@@ -81660,10 +81670,10 @@ function isMcpToolCall(tool) {
81660
81670
  function isApprovalYes(answer) {
81661
81671
  return /^y(es)?$/i.test(answer.trim());
81662
81672
  }
81663
- async function promptUseToolApproval(io, input, meta, resolve, style, gutter = "") {
81673
+ async function promptUseToolApproval(io, input, meta, resolve, style, gutter = "", destructive) {
81664
81674
  const paint = style ?? { yellow: (t) => t, dim: (t) => t, green: (t) => t, red: (t) => t };
81665
81675
  const described = describeUseToolApproval(input, resolve);
81666
- const canTrust = mcpTrustOffered(meta, resolve?.(described.fqn) !== undefined);
81676
+ const canTrust = mcpTrustOffered(meta, resolve?.(described.fqn) !== undefined, destructive?.(described.fqn) === true);
81667
81677
  const lines = renderUseToolApprovalLines(described, meta);
81668
81678
  io.out(`
81669
81679
  `);
@@ -81709,7 +81719,70 @@ function catalogFingerprintResolver(catalog) {
81709
81719
  return entry === undefined ? undefined : toolDefinitionFingerprint(entry);
81710
81720
  };
81711
81721
  }
81712
- var FQN_SEPARATOR = "__", MAX_ARGUMENT_CHARS = 400, MAX_ARGUMENT_KEYS = 40, MAX_IDENTIFIER_CHARS = 80, APPROVAL_ALLOW_ID = "allow";
81722
+ function catalogDestructiveResolver(catalog) {
81723
+ return (fqn) => catalog?.entries.find((candidate) => candidate.fqn === fqn)?.annotations?.destructiveHint === true;
81724
+ }
81725
+ function parseMcpTrustCommand(line) {
81726
+ const parts = line.trim().split(/\s+/);
81727
+ if (parts[0] !== "/mcp" || parts[1] !== "trust")
81728
+ return;
81729
+ const sub = parts[2];
81730
+ const rest = parts.slice(3);
81731
+ if (sub === "list") {
81732
+ return rest.length === 0 ? { kind: "list" } : { kind: "usage", reason: "`/mcp trust list` takes no argument" };
81733
+ }
81734
+ if (sub === "revoke") {
81735
+ if (rest.length === 0)
81736
+ return { kind: "usage", reason: "`/mcp trust revoke` needs a tool name or `all`" };
81737
+ if (rest.length > 1)
81738
+ return { kind: "usage", reason: "`/mcp trust revoke` takes ONE tool name or `all`" };
81739
+ const target = rest[0] ?? "";
81740
+ return target === "all" ? { kind: "revoke-all" } : { kind: "revoke", target };
81741
+ }
81742
+ return { kind: "usage", reason: sub === undefined ? "`/mcp trust` needs a subcommand" : `unknown subcommand \`${sanitiseIdentifier(sub, 40)}\`` };
81743
+ }
81744
+ function catalogTrustStaleness(catalog) {
81745
+ return (fqn, granted) => {
81746
+ const entry = catalog?.entries.find((candidate) => candidate.fqn === fqn);
81747
+ if (entry === undefined)
81748
+ return "gone";
81749
+ if (entry.annotations?.destructiveHint === true)
81750
+ return "destructive";
81751
+ return toolDefinitionFingerprint(entry) === granted ? undefined : "changed";
81752
+ };
81753
+ }
81754
+ function renderTrustedToolLines(trusted, resolve, staleness) {
81755
+ if (trusted === undefined || trusted.size === 0)
81756
+ return ["No MCP tools are trusted in this session."];
81757
+ const lines = [`Trusted MCP tools this session (${trusted.size}):`];
81758
+ for (const [fqn, granted] of [...trusted.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
81759
+ const { server } = resolve?.(fqn) ?? splitFqn(fqn);
81760
+ const stale = staleness?.(fqn, granted);
81761
+ const tail = stale === undefined ? TRUSTED_MARKER : `(will ask again: ${stale})`;
81762
+ lines.push(` ${sanitiseIdentifier(fqn)} server: ${sanitiseIdentifier(server)} ${tail}`);
81763
+ }
81764
+ return lines;
81765
+ }
81766
+ function runMcpTrustCommand(command, trusted, resolve, staleness) {
81767
+ switch (command.kind) {
81768
+ case "usage":
81769
+ return [`${command.reason}. Usage: ${MCP_TRUST_USAGE}`];
81770
+ case "list":
81771
+ return renderTrustedToolLines(trusted, resolve, staleness);
81772
+ case "revoke-all": {
81773
+ const count = trusted?.size ?? 0;
81774
+ trusted?.clear();
81775
+ return [count === 0 ? "No MCP tools were trusted; nothing to revoke." : `Revoked trust for ${count} MCP tool(s); each asks again on its next call.`];
81776
+ }
81777
+ case "revoke": {
81778
+ if (trusted?.delete(command.target) === true) {
81779
+ return [`Revoked trust for ${sanitiseIdentifier(command.target)}; it asks again on its next call.`];
81780
+ }
81781
+ return [`${sanitiseIdentifier(command.target)} is not trusted in this session; nothing changed. Use the full server__tool name (see /mcp trust list).`];
81782
+ }
81783
+ }
81784
+ }
81785
+ var TRUSTED_MARKER = "[trusted]", FQN_SEPARATOR = "__", MAX_ARGUMENT_CHARS = 400, MAX_ARGUMENT_KEYS = 40, MAX_IDENTIFIER_CHARS = 80, APPROVAL_ALLOW_ID = "allow", MCP_TRUST_USAGE = "/mcp trust list | /mcp trust revoke <server__tool> | /mcp trust revoke all";
81713
81786
  var init_approval_render = __esm(() => {
81714
81787
  init_catalog4();
81715
81788
  init_config9();
@@ -92116,7 +92189,7 @@ var init_help_groups = __esm(() => {
92116
92189
  kind: "slash",
92117
92190
  name: "/mcp",
92118
92191
  group: "External agents, ACP and MCP",
92119
- summary: "MCP servers keryx is connected to \u2014 status, connect/disconnect."
92192
+ summary: "MCP servers keryx is connected to \u2014 status, connect/disconnect; /mcp trust list, /mcp trust revoke <server__tool> or all (session trust)."
92120
92193
  },
92121
92194
  {
92122
92195
  kind: "slash",
@@ -97789,7 +97862,7 @@ var init_agent_commands = __esm(() => {
97789
97862
  },
97790
97863
  {
97791
97864
  name: "/mcp",
97792
- description: "MCP servers keryx is connected to \u2014 status, connect/disconnect",
97865
+ description: "MCP servers keryx is connected to \u2014 status, connect/disconnect; /mcp trust list, /mcp trust revoke <server__tool> or all",
97793
97866
  modes: AGENT_ONLY
97794
97867
  },
97795
97868
  {
@@ -112869,6 +112942,7 @@ function buildConsumerModel(input) {
112869
112942
  target: targetOf(server, home),
112870
112943
  credentials: credentialNames(server),
112871
112944
  detail,
112945
+ ...input.trustedByServer?.get(server.name) ? { trustedTools: input.trustedByServer.get(server.name) } : {},
112872
112946
  action: status === "needs-approval" ? `keryx mcp trust ${server.name}` : status === "needs_auth" ? "set the variable named above, then restart the session" : undefined
112873
112947
  };
112874
112948
  });
@@ -112880,6 +112954,9 @@ function buildConsumerModel(input) {
112880
112954
  project: ${collapseHome2(input.projectFile, home)}`
112881
112955
  };
112882
112956
  }
112957
+ function trustedNote(row) {
112958
+ return row.trustedTools !== undefined && row.trustedTools > 0 ? ` ${TRUSTED_MARKER} ${row.trustedTools} tool(s)` : "";
112959
+ }
112883
112960
  function consumerActionFor(status) {
112884
112961
  if (status === "connected")
112885
112962
  return "disconnect";
@@ -112915,7 +112992,7 @@ function formatConsumerModalRow(row, isSelected, status, width) {
112915
112992
  action = "";
112916
112993
  }
112917
112994
  const creds = row.credentials.length > 0 ? ` [reads ${row.credentials.join(", ")}]` : "";
112918
- return wrapHangingRow(`${mark} ${name} ${source} ${transport} ${glyph}`, `${tools}${action} ${row.target}${creds}`, width).join(`
112995
+ return wrapHangingRow(`${mark} ${name} ${source} ${transport} ${glyph}`, `${tools}${action} ${row.target}${creds}${trustedNote(row)}`, width).join(`
112919
112996
  `);
112920
112997
  }
112921
112998
  function formatConsumerModalLines(model, selected, status) {
@@ -117525,6 +117602,7 @@ function createForegroundAgentIoFacade(owner, token, io) {
117525
117602
  permissionMode: () => accepts() ? io.permissionMode?.() ?? "ask" : "ask",
117526
117603
  ...io.trustedMcpTools === undefined ? {} : { trustedMcpTools: io.trustedMcpTools },
117527
117604
  ...io.mcpToolFingerprint === undefined ? {} : { mcpToolFingerprint: io.mcpToolFingerprint },
117605
+ ...io.mcpToolDestructive === undefined ? {} : { mcpToolDestructive: io.mcpToolDestructive },
117528
117606
  readOnly: () => !accepts() || (io.readOnly?.() ?? false)
117529
117607
  };
117530
117608
  }
@@ -128462,10 +128540,11 @@ async function launchTuiAgentShell(opts) {
128462
128540
  }
128463
128541
  if (isMcpToolCall(tool)) {
128464
128542
  const described = describeUseToolApproval(inputJson, catalogResolver(deps.mcpRuntime?.()?.catalog()));
128465
- const canTrustMcp = mcpTrustOffered(meta, catalogResolver(deps.mcpRuntime?.()?.catalog())(described.fqn) !== undefined);
128543
+ const canTrustMcp = mcpTrustOffered(meta, catalogResolver(deps.mcpRuntime?.()?.catalog())(described.fqn) !== undefined, catalogDestructiveResolver(deps.mcpRuntime?.()?.catalog())(described.fqn));
128544
+ const trustedTag = meta?.mcpTrusted === true ? ` ${TRUSTED_MARKER}` : "";
128466
128545
  transcript.add(new otui.TextRenderable(r, {
128467
128546
  id: `ap${uid++}`,
128468
- content: otui.t`${roleChunk(otui, "attention", `\u2699 ${described.server} wants to run ${described.tool}`)}`
128547
+ content: otui.t`${roleChunk(otui, "attention", `\u2699 ${described.server} wants to run ${described.tool}${trustedTag}`)}`
128469
128548
  }));
128470
128549
  for (const line of described.argumentLines) {
128471
128550
  transcript.add(new otui.TextRenderable(r, { id: `ap${uid++}`, content: otui.t`${dimChunk(otui, ` ${line}`)}` }));
@@ -128476,7 +128555,7 @@ async function launchTuiAgentShell(opts) {
128476
128555
  content: otui.t`${dimChunk(otui, ` \u2026 arguments truncated at ${MAX_ARGUMENT_CHARS} characters`)}`
128477
128556
  }));
128478
128557
  }
128479
- for (const notice of untrustedOriginNotices(meta)) {
128558
+ for (const notice of [...untrustedOriginNotices(meta), ...destructiveTrustNotices(meta)]) {
128480
128559
  transcript.add(new otui.TextRenderable(r, { id: `ap${uid++}`, content: otui.t`${dimChunk(otui, ` ${notice}`)}` }));
128481
128560
  }
128482
128561
  chrome.hideMenu();
@@ -128499,7 +128578,7 @@ async function launchTuiAgentShell(opts) {
128499
128578
  setMainAgent("running", allowed ? "write" : "denied");
128500
128579
  transcript.add(new otui.TextRenderable(r, {
128501
128580
  id: `ap${uid++}`,
128502
- content: allowed ? otui.t`${roleChunk(otui, "ok", `\u25C7 ${described.fqn} approved`)}` : otui.t`${roleChunk(otui, "error", `\u25C7 ${described.fqn} denied`)}`
128581
+ content: allowed ? otui.t`${roleChunk(otui, "ok", `\u25C7 ${described.fqn}${trustedTag} approved`)}` : otui.t`${roleChunk(otui, "error", `\u25C7 ${described.fqn}${trustedTag} denied`)}`
128503
128582
  }));
128504
128583
  return verdict;
128505
128584
  }
@@ -128702,6 +128781,7 @@ async function launchTuiAgentShell(opts) {
128702
128781
  io.permissionMode = () => permissionMode;
128703
128782
  io.trustedMcpTools = new Map;
128704
128783
  io.mcpToolFingerprint = (fqn) => catalogFingerprintResolver(deps.mcpRuntime?.()?.catalog())(fqn);
128784
+ io.mcpToolDestructive = (fqn) => catalogDestructiveResolver(deps.mcpRuntime?.()?.catalog())(fqn);
128705
128785
  let readOnly = false;
128706
128786
  io.readOnly = () => readOnly;
128707
128787
  const paintModeRow = () => {
@@ -128710,8 +128790,8 @@ async function launchTuiAgentShell(opts) {
128710
128790
  };
128711
128791
  paintModeRow();
128712
128792
  io.onAutoApproved = (tool, input, meta) => {
128713
- const preview = tool === "shell_exec" ? parseShellExecCommand(input) : tool;
128714
- const label = meta.destructive ? `\u25C7 auto-approved (${permissionMode}) [destructive]` : `\u25C7 auto-approved (${permissionMode})`;
128793
+ const preview = tool === "shell_exec" ? parseShellExecCommand(input) : isMcpToolCall(tool) ? `${tool} ${describeUseToolApproval(input, catalogResolver(deps.mcpRuntime?.()?.catalog())).fqn}` : tool;
128794
+ const label = `\u25C7 auto-approved (${permissionMode})` + (meta.destructive ? " [destructive]" : "") + (meta.mcpTrusted === true ? ` ${TRUSTED_MARKER}` : "");
128715
128795
  transcript.add(new otui.TextRenderable(r, {
128716
128796
  id: `ap${uid++}`,
128717
128797
  content: otui.t`${roleChunk(otui, "attention", label)} ${dimChunk(otui, preview)}`
@@ -129082,6 +129162,7 @@ Staying in the current session.
129082
129162
  return false;
129083
129163
  }
129084
129164
  resetSessionSurface();
129165
+ io.trustedMcpTools?.clear();
129085
129166
  liveSession = opened.handle;
129086
129167
  history = [];
129087
129168
  archive = [];
@@ -129403,13 +129484,32 @@ Staying in the current session.
129403
129484
  client.send(parsed.toLabel, parsed.kind === "ask" ? "question" : "notice", parsed.text).then((result) => io.onSystem?.(`bus: sent (#${result.seq}) to ${parsed.toLabel}
129404
129485
  `)).catch(reportRefusal);
129405
129486
  };
129487
+ const runMcpTrustLine = (line) => {
129488
+ const parsed = parseMcpTrustCommand(line);
129489
+ if (parsed === undefined)
129490
+ return false;
129491
+ const catalog = deps.mcpRuntime?.()?.catalog();
129492
+ const lines = runMcpTrustCommand(parsed, io.trustedMcpTools, catalogResolver(catalog), catalogTrustStaleness(catalog));
129493
+ io.onSystem?.(`${lines.join(`
129494
+ `)}
129495
+ `);
129496
+ return true;
129497
+ };
129406
129498
  const showMcpConsumer = () => {
129407
129499
  const cwd = inspectorCwd();
129408
129500
  const snapshot = () => {
129409
129501
  const live = deps.mcpRuntime?.();
129410
129502
  if (live === undefined)
129411
129503
  return;
129504
+ const resolve = catalogResolver(live.catalog());
129505
+ const trustedByServer = new Map;
129506
+ for (const fqn of io.trustedMcpTools?.keys() ?? []) {
129507
+ const server = resolve(fqn)?.server;
129508
+ if (server !== undefined)
129509
+ trustedByServer.set(server, (trustedByServer.get(server) ?? 0) + 1);
129510
+ }
129412
129511
  return buildConsumerModel({
129512
+ trustedByServer,
129413
129513
  configured: live.configured(),
129414
129514
  states: live.servers(),
129415
129515
  problems: live.problems(),
@@ -130095,7 +130195,8 @@ Staying in the current session.
130095
130195
  return;
130096
130196
  }
130097
130197
  case "mcp-consumer": {
130098
- showMcpConsumer();
130198
+ if (!runMcpTrustLine(line))
130199
+ showMcpConsumer();
130099
130200
  return;
130100
130201
  }
130101
130202
  case "game": {
@@ -130416,7 +130517,8 @@ Staying in the current session.
130416
130517
  return;
130417
130518
  }
130418
130519
  if (isMcpConsumerCommand(command.name)) {
130419
- showMcpConsumer();
130520
+ if (!runMcpTrustLine(line))
130521
+ showMcpConsumer();
130420
130522
  return;
130421
130523
  }
130422
130524
  if (isMcpToolsCommand(command.name)) {
@@ -133265,7 +133367,7 @@ ${GUTTER}${style.dim("[y/N] ")}`);
133265
133367
  return meta?.fingerprint !== undefined ? { approved: true, fingerprint: meta.fingerprint } : true;
133266
133368
  }
133267
133369
  if (isMcpToolCall(tool)) {
133268
- return await promptUseToolApproval({ out, readLine: async () => await readLine() }, input, meta, catalogResolver(deps.mcpRuntime?.()?.catalog()), style, GUTTER);
133370
+ return await promptUseToolApproval({ out, readLine: async () => await readLine() }, input, meta, catalogResolver(deps.mcpRuntime?.()?.catalog()), style, GUTTER, catalogDestructiveResolver(deps.mcpRuntime?.()?.catalog()));
133269
133371
  }
133270
133372
  if (tool !== "shell_exec") {
133271
133373
  const preview = input.length > 120 ? `${input.slice(0, 117)}\u2026` : input;
@@ -133385,14 +133487,16 @@ ${text.trim()}`;
133385
133487
  agentIo.permissionMode = () => permissionMode;
133386
133488
  agentIo.trustedMcpTools = new Map;
133387
133489
  agentIo.mcpToolFingerprint = (fqn) => catalogFingerprintResolver(deps.mcpRuntime?.()?.catalog())(fqn);
133490
+ agentIo.mcpToolDestructive = (fqn) => catalogDestructiveResolver(deps.mcpRuntime?.()?.catalog())(fqn);
133388
133491
  let reasoningSessionOverride;
133389
133492
  let readOnly = false;
133390
133493
  agentIo.readOnly = () => readOnly;
133391
133494
  agentIo.onAutoApproved = (tool, input, meta) => {
133392
133495
  stopSpinner();
133393
- const preview = tool === "shell_exec" ? parseShellExecCommand(input) : tool;
133496
+ const preview = tool === "shell_exec" ? parseShellExecCommand(input) : isMcpToolCall(tool) ? `${tool} ${describeUseToolApproval(input, catalogResolver(deps.mcpRuntime?.()?.catalog())).fqn}` : tool;
133394
133497
  const flag = meta.destructive ? style.yellow(" [destructive]") : "";
133395
- out(`${GUTTER}${style.yellow(`\u25C7 auto-approved (${permissionMode})`)}${flag} ${style.dim(preview)}
133498
+ const trustedFlag = meta.mcpTrusted === true ? style.yellow(` ${TRUSTED_MARKER}`) : "";
133499
+ out(`${GUTTER}${style.yellow(`\u25C7 auto-approved (${permissionMode})`)}${flag}${trustedFlag} ${style.dim(preview)}
133396
133500
  `);
133397
133501
  };
133398
133502
  let live;
@@ -133918,6 +134022,11 @@ ${GUTTER}${turnSeparator()}
133918
134022
  agentIo.onSystem?.(`Usage: /plan [on|off]
133919
134023
  `);
133920
134024
  }
134025
+ } else if (command === "/mcp") {
134026
+ const trust = parseMcpTrustCommand(line);
134027
+ agentIo.onSystem?.(trust === undefined ? "The MCP server view is TUI-only here; `keryx mcp list` shows servers. Session trust: /mcp trust list | revoke <server__tool> | revoke all\n" : `${runMcpTrustCommand(trust, agentIo.trustedMcpTools, catalogResolver(deps.mcpRuntime?.()?.catalog()), catalogTrustStaleness(deps.mcpRuntime?.()?.catalog())).join(`
134028
+ `)}
134029
+ `);
133921
134030
  } else if (command === "/search-provider") {
133922
134031
  const args = parseSearchProviderArgs2(parts.slice(1));
133923
134032
  const all = searchProviderController.configurable();
@@ -134894,6 +135003,7 @@ var init_shell = __esm(() => {
134894
135003
  "/mode",
134895
135004
  "/reasoning",
134896
135005
  "/plan",
135006
+ "/mcp",
134897
135007
  "/exit"
134898
135008
  ];
134899
135009
  SPINNER_FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
package/dist/core.js CHANGED
@@ -38473,7 +38473,7 @@ var HELP_GROUPS = [
38473
38473
  kind: "slash",
38474
38474
  name: "/mcp",
38475
38475
  group: "External agents, ACP and MCP",
38476
- summary: "MCP servers keryx is connected to \u2014 status, connect/disconnect."
38476
+ summary: "MCP servers keryx is connected to \u2014 status, connect/disconnect; /mcp trust list, /mcp trust revoke <server__tool> or all (session trust)."
38477
38477
  },
38478
38478
  {
38479
38479
  kind: "slash",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.34",
3
+ "version": "0.3.35",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {