@gaunt-sloth/core 2.0.0-alpha.8 → 2.0.0-beta.0

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.
Files changed (275) hide show
  1. package/.gsloth.review.md +2 -0
  2. package/README.md +71 -20
  3. package/dist/config/colour.d.ts +38 -0
  4. package/dist/config/colour.js +36 -0
  5. package/dist/config/colour.js.map +1 -0
  6. package/dist/config/configDiscovery.d.ts +79 -0
  7. package/dist/config/configDiscovery.js +80 -0
  8. package/dist/config/configDiscovery.js.map +1 -0
  9. package/dist/config/defaults.d.ts +20 -20
  10. package/dist/config/defaults.js +10 -8
  11. package/dist/config/defaults.js.map +1 -1
  12. package/dist/config/filesystem-tools.d.ts +41 -0
  13. package/dist/config/filesystem-tools.js +56 -0
  14. package/dist/config/filesystem-tools.js.map +1 -0
  15. package/dist/config/loader.d.ts +171 -19
  16. package/dist/config/loader.js +1054 -144
  17. package/dist/config/loader.js.map +1 -1
  18. package/dist/config/mouse.d.ts +50 -0
  19. package/dist/config/mouse.js +44 -0
  20. package/dist/config/mouse.js.map +1 -0
  21. package/dist/config/profiles.d.ts +68 -0
  22. package/dist/config/profiles.js +93 -0
  23. package/dist/config/profiles.js.map +1 -0
  24. package/dist/config/providerKeys.d.ts +69 -0
  25. package/dist/config/providerKeys.js +69 -0
  26. package/dist/config/providerKeys.js.map +1 -0
  27. package/dist/config/schema.d.ts +2671 -138
  28. package/dist/config/schema.js +1361 -85
  29. package/dist/config/schema.js.map +1 -1
  30. package/dist/config/shell-policy.d.ts +899 -111
  31. package/dist/config/shell-policy.js +800 -70
  32. package/dist/config/shell-policy.js.map +1 -1
  33. package/dist/config/tool-descriptions.d.ts +211 -0
  34. package/dist/config/tool-descriptions.js +272 -0
  35. package/dist/config/tool-descriptions.js.map +1 -0
  36. package/dist/config/types.d.ts +352 -41
  37. package/dist/config/types.js +1 -0
  38. package/dist/config/types.js.map +1 -1
  39. package/dist/config.d.ts +35 -1
  40. package/dist/config.js +16 -1
  41. package/dist/config.js.map +1 -1
  42. package/dist/constants.d.ts +28 -1
  43. package/dist/constants.js +28 -1
  44. package/dist/constants.js.map +1 -1
  45. package/dist/core/GthAbstractAgent.d.ts +166 -11
  46. package/dist/core/GthAbstractAgent.js +484 -45
  47. package/dist/core/GthAbstractAgent.js.map +1 -1
  48. package/dist/core/GthAgentRunner.d.ts +543 -57
  49. package/dist/core/GthAgentRunner.js +1494 -140
  50. package/dist/core/GthAgentRunner.js.map +1 -1
  51. package/dist/core/GthLangChainAgent.d.ts +117 -2
  52. package/dist/core/GthLangChainAgent.js +602 -29
  53. package/dist/core/GthLangChainAgent.js.map +1 -1
  54. package/dist/core/approvals/annotations.d.ts +122 -0
  55. package/dist/core/approvals/annotations.js +137 -0
  56. package/dist/core/approvals/annotations.js.map +1 -0
  57. package/dist/core/approvals/grants.d.ts +216 -0
  58. package/dist/core/approvals/grants.js +469 -0
  59. package/dist/core/approvals/grants.js.map +1 -0
  60. package/dist/core/approvals/matcher.d.ts +202 -0
  61. package/dist/core/approvals/matcher.js +267 -0
  62. package/dist/core/approvals/matcher.js.map +1 -0
  63. package/dist/core/approvals/mcpSubjects.d.ts +40 -0
  64. package/dist/core/approvals/mcpSubjects.js +99 -0
  65. package/dist/core/approvals/mcpSubjects.js.map +1 -0
  66. package/dist/core/approvals/promptHeader.d.ts +28 -0
  67. package/dist/core/approvals/promptHeader.js +62 -0
  68. package/dist/core/approvals/promptHeader.js.map +1 -0
  69. package/dist/core/approvals/toolAnnotationSources.d.ts +105 -0
  70. package/dist/core/approvals/toolAnnotationSources.js +277 -0
  71. package/dist/core/approvals/toolAnnotationSources.js.map +1 -0
  72. package/dist/core/approvals/toolHost.d.ts +46 -0
  73. package/dist/core/approvals/toolHost.js +108 -0
  74. package/dist/core/approvals/toolHost.js.map +1 -0
  75. package/dist/core/debugCapture.d.ts +74 -0
  76. package/dist/core/debugCapture.js +100 -0
  77. package/dist/core/debugCapture.js.map +1 -0
  78. package/dist/core/gthLeanAgentFactory.d.ts +4 -4
  79. package/dist/core/gthLeanAgentFactory.js +4 -4
  80. package/dist/core/launchBanner.d.ts +127 -0
  81. package/dist/core/launchBanner.js +414 -0
  82. package/dist/core/launchBanner.js.map +1 -0
  83. package/dist/core/modelLabel.d.ts +19 -0
  84. package/dist/core/modelLabel.js +26 -0
  85. package/dist/core/modelLabel.js.map +1 -0
  86. package/dist/core/plainToolIndication.d.ts +15 -0
  87. package/dist/core/plainToolIndication.js +174 -0
  88. package/dist/core/plainToolIndication.js.map +1 -0
  89. package/dist/core/reasoningBlocks.d.ts +65 -0
  90. package/dist/core/reasoningBlocks.js +103 -0
  91. package/dist/core/reasoningBlocks.js.map +1 -0
  92. package/dist/core/refusal.d.ts +53 -0
  93. package/dist/core/refusal.js +133 -0
  94. package/dist/core/refusal.js.map +1 -0
  95. package/dist/core/runHeader.d.ts +38 -0
  96. package/dist/core/runHeader.js +42 -0
  97. package/dist/core/runHeader.js.map +1 -0
  98. package/dist/core/runStats.d.ts +14 -3
  99. package/dist/core/runStats.js +48 -3
  100. package/dist/core/runStats.js.map +1 -1
  101. package/dist/core/shell/ShellCommandFailedError.d.ts +3 -4
  102. package/dist/core/shell/ShellCommandFailedError.js +3 -4
  103. package/dist/core/shell/ShellCommandFailedError.js.map +1 -1
  104. package/dist/core/shell/abstention.d.ts +88 -0
  105. package/dist/core/shell/abstention.js +184 -0
  106. package/dist/core/shell/abstention.js.map +1 -0
  107. package/dist/core/shell/approvalCapture.d.ts +271 -0
  108. package/dist/core/shell/approvalCapture.js +108 -0
  109. package/dist/core/shell/approvalCapture.js.map +1 -0
  110. package/dist/core/shell/approvalStop.d.ts +123 -0
  111. package/dist/core/shell/approvalStop.js +269 -0
  112. package/dist/core/shell/approvalStop.js.map +1 -0
  113. package/dist/core/shell/arity.d.ts +6 -0
  114. package/dist/core/shell/arity.js +20 -6
  115. package/dist/core/shell/arity.js.map +1 -1
  116. package/dist/core/shell/denylist.d.ts +11 -0
  117. package/dist/core/shell/denylist.js +37 -0
  118. package/dist/core/shell/denylist.js.map +1 -0
  119. package/dist/core/shell/escalationSeverity.d.ts +141 -0
  120. package/dist/core/shell/escalationSeverity.js +89 -0
  121. package/dist/core/shell/escalationSeverity.js.map +1 -0
  122. package/dist/core/shell/framing.d.ts +190 -0
  123. package/dist/core/shell/framing.js +633 -0
  124. package/dist/core/shell/framing.js.map +1 -0
  125. package/dist/core/shell/hardline.d.ts +103 -0
  126. package/dist/core/shell/hardline.js +780 -0
  127. package/dist/core/shell/hardline.js.map +1 -0
  128. package/dist/core/shell/negotiation.d.ts +328 -0
  129. package/dist/core/shell/negotiation.js +488 -0
  130. package/dist/core/shell/negotiation.js.map +1 -0
  131. package/dist/core/shell/normalize.d.ts +44 -4
  132. package/dist/core/shell/normalize.js +61 -7
  133. package/dist/core/shell/normalize.js.map +1 -1
  134. package/dist/core/shell/openWorld.d.ts +263 -0
  135. package/dist/core/shell/openWorld.js +1188 -0
  136. package/dist/core/shell/openWorld.js.map +1 -0
  137. package/dist/core/shell/rater.d.ts +873 -0
  138. package/dist/core/shell/rater.js +1454 -0
  139. package/dist/core/shell/rater.js.map +1 -0
  140. package/dist/core/shell/raterModel.d.ts +41 -0
  141. package/dist/core/shell/raterModel.js +51 -0
  142. package/dist/core/shell/raterModel.js.map +1 -0
  143. package/dist/core/shell/raterVocabulary.d.ts +121 -0
  144. package/dist/core/shell/raterVocabulary.js +116 -0
  145. package/dist/core/shell/raterVocabulary.js.map +1 -0
  146. package/dist/core/shell/rejection.d.ts +69 -0
  147. package/dist/core/shell/rejection.js +38 -0
  148. package/dist/core/shell/rejection.js.map +1 -0
  149. package/dist/core/toolCallRepair/grammar.d.ts +41 -0
  150. package/dist/core/toolCallRepair/grammar.js +116 -0
  151. package/dist/core/toolCallRepair/grammar.js.map +1 -0
  152. package/dist/core/toolCallRepair/index.d.ts +2 -0
  153. package/dist/core/toolCallRepair/index.js +7 -0
  154. package/dist/core/toolCallRepair/index.js.map +1 -0
  155. package/dist/core/toolCallRepair/payload.d.ts +36 -0
  156. package/dist/core/toolCallRepair/payload.js +341 -0
  157. package/dist/core/toolCallRepair/payload.js.map +1 -0
  158. package/dist/core/toolCallRepair/promote.d.ts +45 -0
  159. package/dist/core/toolCallRepair/promote.js +90 -0
  160. package/dist/core/toolCallRepair/promote.js.map +1 -0
  161. package/dist/core/toolDisplay.d.ts +123 -0
  162. package/dist/core/toolDisplay.js +451 -0
  163. package/dist/core/toolDisplay.js.map +1 -0
  164. package/dist/core/toolOutputChannel.d.ts +95 -0
  165. package/dist/core/toolOutputChannel.js +165 -0
  166. package/dist/core/toolOutputChannel.js.map +1 -0
  167. package/dist/core/types.d.ts +349 -16
  168. package/dist/core/types.js.map +1 -1
  169. package/dist/history/historyFormat.d.ts +12 -3
  170. package/dist/history/historyFormat.js +50 -8
  171. package/dist/history/historyFormat.js.map +1 -1
  172. package/dist/history/historyStore.d.ts +77 -0
  173. package/dist/history/historyStore.js +173 -6
  174. package/dist/history/historyStore.js.map +1 -1
  175. package/dist/history/recordSession.d.ts +10 -1
  176. package/dist/history/recordSession.js +27 -0
  177. package/dist/history/recordSession.js.map +1 -1
  178. package/dist/providers/anthropic.js +12 -0
  179. package/dist/providers/anthropic.js.map +1 -1
  180. package/dist/providers/configurationPassthrough.d.ts +107 -0
  181. package/dist/providers/configurationPassthrough.js +148 -0
  182. package/dist/providers/configurationPassthrough.js.map +1 -0
  183. package/dist/providers/geminiSchemaSanitizer.d.ts +55 -0
  184. package/dist/providers/geminiSchemaSanitizer.js +347 -0
  185. package/dist/providers/geminiSchemaSanitizer.js.map +1 -0
  186. package/dist/providers/geminiThinking.d.ts +60 -0
  187. package/dist/providers/geminiThinking.js +92 -0
  188. package/dist/providers/geminiThinking.js.map +1 -0
  189. package/dist/providers/google-genai.js +18 -1
  190. package/dist/providers/google-genai.js.map +1 -1
  191. package/dist/providers/groq.js +12 -0
  192. package/dist/providers/groq.js.map +1 -1
  193. package/dist/providers/huggingface.d.ts +25 -0
  194. package/dist/providers/huggingface.js +69 -0
  195. package/dist/providers/huggingface.js.map +1 -0
  196. package/dist/providers/modelCatalog.d.ts +109 -0
  197. package/dist/providers/modelCatalog.js +245 -0
  198. package/dist/providers/modelCatalog.js.map +1 -0
  199. package/dist/providers/modelDiscovery.d.ts +99 -5
  200. package/dist/providers/modelDiscovery.js +191 -35
  201. package/dist/providers/modelDiscovery.js.map +1 -1
  202. package/dist/providers/ollama.d.ts +18 -4
  203. package/dist/providers/ollama.js +67 -37
  204. package/dist/providers/ollama.js.map +1 -1
  205. package/dist/providers/openai.js +34 -0
  206. package/dist/providers/openai.js.map +1 -1
  207. package/dist/providers/openrouter.d.ts +26 -4
  208. package/dist/providers/openrouter.js +83 -26
  209. package/dist/providers/openrouter.js.map +1 -1
  210. package/dist/providers/vertexai.js +19 -1
  211. package/dist/providers/vertexai.js.map +1 -1
  212. package/dist/providers/xai.js +20 -0
  213. package/dist/providers/xai.js.map +1 -1
  214. package/dist/runtime/askStructured.d.ts +105 -0
  215. package/dist/runtime/askStructured.js +120 -0
  216. package/dist/runtime/askStructured.js.map +1 -0
  217. package/dist/runtime/conversation.d.ts +64 -0
  218. package/dist/runtime/conversation.js +171 -0
  219. package/dist/runtime/conversation.js.map +1 -0
  220. package/dist/runtime/singleShot.d.ts +37 -6
  221. package/dist/runtime/singleShot.js +113 -67
  222. package/dist/runtime/singleShot.js.map +1 -1
  223. package/dist/runtime/structuredOutput.d.ts +104 -0
  224. package/dist/runtime/structuredOutput.js +393 -0
  225. package/dist/runtime/structuredOutput.js.map +1 -0
  226. package/dist/utils/ProgressIndicator.d.ts +21 -0
  227. package/dist/utils/ProgressIndicator.js +30 -3
  228. package/dist/utils/ProgressIndicator.js.map +1 -1
  229. package/dist/utils/aiignoreUtils.js.map +1 -1
  230. package/dist/utils/binaryOutputUtils.js.map +1 -1
  231. package/dist/utils/consoleUtils.d.ts +95 -0
  232. package/dist/utils/consoleUtils.js +112 -2
  233. package/dist/utils/consoleUtils.js.map +1 -1
  234. package/dist/utils/crashHandler.d.ts +87 -0
  235. package/dist/utils/crashHandler.js +128 -0
  236. package/dist/utils/crashHandler.js.map +1 -0
  237. package/dist/utils/debugDump.d.ts +134 -0
  238. package/dist/utils/debugDump.js +381 -0
  239. package/dist/utils/debugDump.js.map +1 -0
  240. package/dist/utils/debugUtils.d.ts +13 -4
  241. package/dist/utils/debugUtils.js +36 -13
  242. package/dist/utils/debugUtils.js.map +1 -1
  243. package/dist/utils/displayWidth.d.ts +53 -0
  244. package/dist/utils/displayWidth.js +195 -0
  245. package/dist/utils/displayWidth.js.map +1 -0
  246. package/dist/utils/fileUtils.js.map +1 -1
  247. package/dist/utils/globalConfigUtils.d.ts +14 -2
  248. package/dist/utils/globalConfigUtils.js +22 -4
  249. package/dist/utils/globalConfigUtils.js.map +1 -1
  250. package/dist/utils/llmUtils.d.ts +39 -8
  251. package/dist/utils/llmUtils.js +76 -8
  252. package/dist/utils/llmUtils.js.map +1 -1
  253. package/dist/utils/redactSecrets.d.ts +63 -0
  254. package/dist/utils/redactSecrets.js +286 -0
  255. package/dist/utils/redactSecrets.js.map +1 -0
  256. package/dist/utils/systemPromptNotes.d.ts +222 -0
  257. package/dist/utils/systemPromptNotes.js +338 -0
  258. package/dist/utils/systemPromptNotes.js.map +1 -0
  259. package/dist/utils/systemUtils.d.ts +18 -1
  260. package/dist/utils/systemUtils.js +38 -3
  261. package/dist/utils/systemUtils.js.map +1 -1
  262. package/dist/utils/toolMatching.d.ts +30 -0
  263. package/dist/utils/toolMatching.js +44 -0
  264. package/dist/utils/toolMatching.js.map +1 -0
  265. package/dist/utils/untrustedText.d.ts +86 -0
  266. package/dist/utils/untrustedText.js +101 -0
  267. package/dist/utils/untrustedText.js.map +1 -0
  268. package/package.json +21 -7
  269. package/schema/gsloth-config.schema.json +1921 -326
  270. package/dist/core/shell/allowlist.d.ts +0 -75
  271. package/dist/core/shell/allowlist.js +0 -187
  272. package/dist/core/shell/allowlist.js.map +0 -1
  273. package/dist/core/shell/judge.d.ts +0 -161
  274. package/dist/core/shell/judge.js +0 -261
  275. package/dist/core/shell/judge.js.map +0 -1
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @module core/approvals/matcher
3
+ *
4
+ * EXT-71 (spec §3.1, §3.2, §3.3) — **the one comparison engine** over {@link ApprovalEntry}, and
5
+ * the three-list resolution built on top of it. Every approvals decision that is not the rung's own
6
+ * default runs through {@link resolveApprovalRules}; there is no second matcher anywhere.
7
+ *
8
+ * ## What an entry compares against
9
+ *
10
+ * `exact` / `glob` / `regexp` compare against the **normalized command** for a `shell` entry and
11
+ * against the **tool name** for `tool` / `mcpTool`. The normalization is
12
+ * {@link import('../shell/normalize.js').normalizeCommand} — the same one the command classifier and
13
+ * the §8 floor already use, never a second one of this module's own, because a second normalization
14
+ * is how a matcher and a floor come to disagree about what a command is.
15
+ *
16
+ * `hint` is valid only on tool subjects and reads the call's **effective** annotations through
17
+ * {@link EffectiveToolAnnotationSource}, which `core/approvals/annotations.ts` builds from the user's
18
+ * per-server trust (§4.7.1). A caller that wires no source gets {@link failClosedToolAnnotations}:
19
+ * the MCP fail-closed defaults, which describe a tool that writes, destroys, is not idempotent and
20
+ * reaches the open world.
21
+ *
22
+ * ## The asymmetry (§3.1 "Compound commands"), which is the whole point of this module
23
+ *
24
+ * **No allow entry of any matcher matches a command that does not statically resolve.** If
25
+ * {@link classifyCommand} cannot resolve it — composition, substitution, redirection — it is a
26
+ * non-match for `exact`, `glob` and `regexp` alike, so a glob can never span a command separator and
27
+ * `git *` never grants `git status && curl x | sh`.
28
+ *
29
+ * **A deny (or escalate) entry MAY match a compound command**, and is matched against every segment
30
+ * a shell would run as well as against the whole string, because a prohibition that catches
31
+ * something unresolvable errs in the direction that costs nothing.
32
+ *
33
+ * The same split governs a match the engine cannot decide (a regexp over its run-time budget, or an
34
+ * annotation set nothing can supply): **undecidable is a non-match on the allow side and a match on
35
+ * the restrictive side.** Each fails toward a prompt, never toward an execution.
36
+ *
37
+ * ## Resolution
38
+ *
39
+ * `deny` > `escalate` > `allow`, most-restrictive-wins: every deny entry is consulted before any
40
+ * escalate entry and every escalate entry before any allow entry, so **author order and merge order
41
+ * cannot change the outcome** and an appended entry can never perturb an existing one. Within a
42
+ * list the first match is reported, which decides only the provenance shown to the user.
43
+ *
44
+ * ## Case
45
+ *
46
+ * Shell `exact` / `glob` comparisons on the restrictive lists fold case, matching the established
47
+ * deny behaviour (`core/shell/denylist.ts`) — broader on a list whose breadth is fail-safe. Allow
48
+ * comparisons, tool names, and **every `regexp` on every list** are compared as written: silently
49
+ * adding an `i` flag to a pattern the user authored would make it mean something they did not write.
50
+ */
51
+ import { type ApprovalEntry, type ShellApprovalGateNotice } from '#src/config/shell-policy.js';
52
+ /** A gated `run_shell_command` call: the command exactly as the model proposed it. */
53
+ export interface ShellApprovalSubject {
54
+ kind: 'shell';
55
+ /** RAW command. Normalization happens here so no caller can normalize differently. */
56
+ command: string;
57
+ }
58
+ /** A gated built-in / custom in-process tool call. */
59
+ export interface ToolApprovalSubject {
60
+ kind: 'tool';
61
+ name: string;
62
+ /** §4.7.4 — the host this call reaches, where it has one. */
63
+ host?: string;
64
+ }
65
+ /**
66
+ * A gated MCP tool call. `server` is the user's own key in `mcpServers` (§4.7.5) — the only identity
67
+ * a server has that is stable, unique and user-authored, and the key its trust and its rule entries
68
+ * are both written against. Nothing a server declares about its own name ever participates.
69
+ */
70
+ export interface McpToolApprovalSubject {
71
+ kind: 'mcpTool';
72
+ server: string;
73
+ name: string;
74
+ host?: string;
75
+ }
76
+ /** Any subject the matcher can be asked about. */
77
+ export type ApprovalSubject = ShellApprovalSubject | ToolApprovalSubject | McpToolApprovalSubject;
78
+ /**
79
+ * §4.7.1 — the four MCP `ToolAnnotations` booleans, as they **effectively** hold for a call after
80
+ * provenance and trust have been applied. All four are present: a hint entry asks a yes/no question
81
+ * of each name it uses, so "unknown" is expressed by the source returning `undefined` for the whole
82
+ * set, never by a missing key.
83
+ */
84
+ export interface EffectiveToolAnnotations {
85
+ readOnlyHint: boolean;
86
+ destructiveHint: boolean;
87
+ idempotentHint: boolean;
88
+ openWorldHint: boolean;
89
+ }
90
+ /**
91
+ * Resolves the effective annotations of a tool call, or `undefined` when they cannot be determined
92
+ * — which the matcher treats as undecidable (non-match on allow, match on deny/escalate).
93
+ *
94
+ * This function type is the whole contract between the matcher and the trust model:
95
+ * `createEffectiveToolAnnotationSource` (`core/approvals/annotations.ts`) is the one implementation
96
+ * that applies per-server, per-hint trust (§4.7.1), and it is the ONLY place an effective set is
97
+ * derived. Nothing in this module knows how trust is decided, which is what lets the two evolve
98
+ * independently.
99
+ */
100
+ export type EffectiveToolAnnotationSource = (subject: ToolApprovalSubject | McpToolApprovalSubject) => EffectiveToolAnnotations | undefined;
101
+ /**
102
+ * The MCP fail-closed defaults (spec §4.7.2): a tool that has said nothing about itself is assumed
103
+ * to write, to destroy, to be non-idempotent and to reach the open world. These are the values the
104
+ * MCP specification itself defines for absent annotations, so an entry written against them is
105
+ * written against the protocol's own conservative reading.
106
+ *
107
+ * **Frozen**, and read-only to the type system, because it is shared: an effective set is something
108
+ * callers snapshot (§4.7.4 records one on a sticky grant), so a source that handed this object out
109
+ * instead of a copy would let one caller's snapshot rewrite the fail-closed floor for every other.
110
+ * Freezing turns that aliasing bug into an immediate throw rather than a silent, global loosening.
111
+ */
112
+ export declare const MCP_FAIL_CLOSED_ANNOTATIONS: Readonly<EffectiveToolAnnotations>;
113
+ /**
114
+ * The {@link EffectiveToolAnnotationSource} a caller gets when it wires none: every tool reads as
115
+ * {@link MCP_FAIL_CLOSED_ANNOTATIONS}.
116
+ *
117
+ * It is the safe default and not a stub — it is what a fully distrustful configuration computes
118
+ * anyway (§4.7.1: an untrusted server's effective set IS this constant), so a call site that has no
119
+ * annotations to offer behaves exactly like one whose user believes nothing. Deliberately NOT a
120
+ * trust model: trust needs config, and config belongs to `core/approvals/annotations.ts`.
121
+ *
122
+ * It answers with a **fresh object per call**, matching `createEffectiveToolAnnotationSource`, so
123
+ * the whole contract — not merely its configured half — is safe to snapshot.
124
+ */
125
+ export declare const failClosedToolAnnotations: EffectiveToolAnnotationSource;
126
+ /**
127
+ * §3.1 — the run-time budget for ONE `regexp` entry evaluation, in milliseconds.
128
+ *
129
+ * It is a **backstop reported rather than swallowed**, not the primary defence: a pattern is
130
+ * length-capped and compile-checked when the config loads (`approvalEntrySchema`), and a short
131
+ * pattern can still backtrack catastrophically. What this bounds is the damage — and, more
132
+ * importantly, it makes a pattern that stopped deciding *visible*, so a session in which one quietly
133
+ * fell out of use cannot be mistaken for one in which it works.
134
+ */
135
+ export declare const REGEXP_MATCH_BUDGET_MS = 50;
136
+ /** Tuning + seams for {@link resolveApprovalRules}. All optional; the defaults are the product's. */
137
+ export interface ApprovalMatcherOptions {
138
+ /**
139
+ * Where a `hint` entry reads its effective values (§4.7.1). Build it with
140
+ * `createEffectiveToolAnnotationSource`; defaults to {@link failClosedToolAnnotations}.
141
+ */
142
+ annotations?: EffectiveToolAnnotationSource;
143
+ /**
144
+ * Where a run-time report goes — the established {@link ShellApprovalGateNotice} shape, which the
145
+ * runner forwards to `statusUpdate`. Absent means the report is dropped, which is why the runner
146
+ * always passes one.
147
+ */
148
+ onNotice?: (notice: ShellApprovalGateNotice) => void;
149
+ /** Override {@link REGEXP_MATCH_BUDGET_MS}. */
150
+ regexpBudgetMs?: number;
151
+ /** Clock, injectable so the budget backstop is testable without a pathological pattern. */
152
+ now?: () => number;
153
+ }
154
+ /**
155
+ * §3.2 — whether the rater still reviews a call this entry matched. Honored at the rater rungs and
156
+ * inert at the deterministic ones; a per-entry `rate` wins in **both** directions.
157
+ *
158
+ * The default derives from one principle: *an entry skips the rater only to the extent that it
159
+ * recorded what the rater would have seen.* A `shell` + `exact` entry recorded the whole command, so
160
+ * it needs no rating; a pattern recorded a shape, and a tool entry recorded identity (never
161
+ * arguments), so both keep the rater.
162
+ */
163
+ export declare function approvalEntryRatesCall(entry: ApprovalEntry): boolean;
164
+ /** The three declared lists, as one argument. */
165
+ export interface ApprovalRuleLists {
166
+ allow: readonly ApprovalEntry[];
167
+ deny: readonly ApprovalEntry[];
168
+ escalate: readonly ApprovalEntry[];
169
+ }
170
+ /** What the rule lists decided about a call, or `null` when no entry matched. */
171
+ export interface ApprovalRuleDecision {
172
+ /** §3 — most-restrictive-wins: `deny` over `escalate` over `allow`. */
173
+ action: 'deny' | 'escalate' | 'allow';
174
+ /** The entry that fired, for the provenance the prompt and the refusal message must show. */
175
+ entry: ApprovalEntry;
176
+ /**
177
+ * §3.2 — for an `allow` action, whether the rater still reviews the call as a **tripwire**.
178
+ * Always `false` for `deny` (nothing to rate) and for `escalate` (which goes straight to the
179
+ * human with no rating call, because the user pre-decided that a human answers).
180
+ */
181
+ rate: boolean;
182
+ }
183
+ /**
184
+ * §3.3 — resolve a call against all three lists, most-restrictive-wins.
185
+ *
186
+ * **Every** deny entry is consulted before **any** escalate entry, and every escalate entry before
187
+ * any allow entry. That ordering is the whole property: author order within a list, and the order
188
+ * in which lists were concatenated, cannot change the outcome, and appending an entry can never
189
+ * perturb an existing one. The first match inside a list decides only which entry is reported.
190
+ *
191
+ * Returns `null` when nothing matched — the rung then decides on its own.
192
+ */
193
+ export declare function resolveApprovalRules(subject: ApprovalSubject, lists: ApprovalRuleLists, options?: ApprovalMatcherOptions): ApprovalRuleDecision | null;
194
+ /**
195
+ * A one-line rendering of an entry, for the provenance a refusal message and an escalation prompt
196
+ * must carry (§3, §3.2) and for the `/approvals` list display.
197
+ *
198
+ * A `shell` + `exact` entry renders as its bare pattern — that is the command the user wrote, and
199
+ * decorating it would only get between them and the line they are looking for. Everything else says
200
+ * what kind of thing it is, because a pattern that is not the command needs to be readable as one.
201
+ */
202
+ export declare function describeApprovalEntry(entry: ApprovalEntry): string;
@@ -0,0 +1,267 @@
1
+ import { classifyCommand } from '#src/core/shell/arity.js';
2
+ import { normalizeCommand } from '#src/core/shell/normalize.js';
3
+ import { commandSegments } from '#src/core/shell/denylist.js';
4
+ import { StatusLevel } from '#src/core/types.js';
5
+ /**
6
+ * The MCP fail-closed defaults (spec §4.7.2): a tool that has said nothing about itself is assumed
7
+ * to write, to destroy, to be non-idempotent and to reach the open world. These are the values the
8
+ * MCP specification itself defines for absent annotations, so an entry written against them is
9
+ * written against the protocol's own conservative reading.
10
+ *
11
+ * **Frozen**, and read-only to the type system, because it is shared: an effective set is something
12
+ * callers snapshot (§4.7.4 records one on a sticky grant), so a source that handed this object out
13
+ * instead of a copy would let one caller's snapshot rewrite the fail-closed floor for every other.
14
+ * Freezing turns that aliasing bug into an immediate throw rather than a silent, global loosening.
15
+ */
16
+ export const MCP_FAIL_CLOSED_ANNOTATIONS = Object.freeze({
17
+ readOnlyHint: false,
18
+ destructiveHint: true,
19
+ idempotentHint: false,
20
+ openWorldHint: true,
21
+ });
22
+ /**
23
+ * The {@link EffectiveToolAnnotationSource} a caller gets when it wires none: every tool reads as
24
+ * {@link MCP_FAIL_CLOSED_ANNOTATIONS}.
25
+ *
26
+ * It is the safe default and not a stub — it is what a fully distrustful configuration computes
27
+ * anyway (§4.7.1: an untrusted server's effective set IS this constant), so a call site that has no
28
+ * annotations to offer behaves exactly like one whose user believes nothing. Deliberately NOT a
29
+ * trust model: trust needs config, and config belongs to `core/approvals/annotations.ts`.
30
+ *
31
+ * It answers with a **fresh object per call**, matching `createEffectiveToolAnnotationSource`, so
32
+ * the whole contract — not merely its configured half — is safe to snapshot.
33
+ */
34
+ export const failClosedToolAnnotations = () => ({
35
+ ...MCP_FAIL_CLOSED_ANNOTATIONS,
36
+ });
37
+ /* -------------------------------------------------------------------------------------------- *
38
+ * The engine.
39
+ * -------------------------------------------------------------------------------------------- */
40
+ /**
41
+ * §3.1 — the run-time budget for ONE `regexp` entry evaluation, in milliseconds.
42
+ *
43
+ * It is a **backstop reported rather than swallowed**, not the primary defence: a pattern is
44
+ * length-capped and compile-checked when the config loads (`approvalEntrySchema`), and a short
45
+ * pattern can still backtrack catastrophically. What this bounds is the damage — and, more
46
+ * importantly, it makes a pattern that stopped deciding *visible*, so a session in which one quietly
47
+ * fell out of use cannot be mistaken for one in which it works.
48
+ */
49
+ export const REGEXP_MATCH_BUDGET_MS = 50;
50
+ function contextFrom(options) {
51
+ return {
52
+ annotations: options?.annotations ?? failClosedToolAnnotations,
53
+ onNotice: options?.onNotice,
54
+ regexpBudgetMs: options?.regexpBudgetMs ?? REGEXP_MATCH_BUDGET_MS,
55
+ now: options?.now ?? Date.now,
56
+ };
57
+ }
58
+ /** Every regexp metacharacter EXCEPT `*`, which the glob translation owns. */
59
+ const GLOB_METACHARACTERS = /[.+?^${}()|[\]\\]/g;
60
+ /**
61
+ * §3.1 — compile a glob pattern. `*` matches any run of characters **including none**, matched
62
+ * against the **whole** string rather than token by token; everything else is literal.
63
+ *
64
+ * The consequence worth stating, because it is the first thing anyone gets wrong: `npm publish *`
65
+ * does NOT match a bare `npm publish` — the space before the `*` is part of the pattern.
66
+ * `npm publish*` matches both, and is almost always what was meant.
67
+ *
68
+ * The `s` flag is deliberate: a normalized compound command contains real line breaks (EXT-55 keeps
69
+ * them, because a line break is a command separator), and on the restrictive lists a `*` must be
70
+ * able to cross one. It can never do so on the allow side, where a command containing a separator
71
+ * has already been refused as unresolvable.
72
+ */
73
+ function globToRegExp(pattern) {
74
+ const escaped = pattern.replace(GLOB_METACHARACTERS, '\\$&').replace(/\*/g, '.*');
75
+ return new RegExp(`^${escaped}$`, 's');
76
+ }
77
+ /**
78
+ * Evaluate a `regexp` entry against one candidate string, under the run-time budget.
79
+ *
80
+ * The pattern compiles as written — no flags are added. Silently making a user's pattern
81
+ * case-insensitive would make it mean something they did not write, on a list where breadth has
82
+ * consequences in both directions.
83
+ */
84
+ function testRegexpPattern(pattern, candidate, ctx) {
85
+ let compiled;
86
+ try {
87
+ compiled = new RegExp(pattern);
88
+ }
89
+ catch {
90
+ // Unreachable via config (the schema compile-checks every pattern at load) and deliberately not
91
+ // an exception: a pattern that cannot be compiled cannot be decided, which the caller already
92
+ // knows how to fail safely on.
93
+ return 'undecidable';
94
+ }
95
+ const started = ctx.now();
96
+ const matched = compiled.test(candidate);
97
+ const elapsed = ctx.now() - started;
98
+ if (elapsed > ctx.regexpBudgetMs) {
99
+ ctx.onNotice?.({
100
+ level: StatusLevel.WARNING,
101
+ message: `An approvals regexp entry took ${elapsed}ms to evaluate, over its ${ctx.regexpBudgetMs}ms ` +
102
+ `budget, so it was treated as undecided for this call: ${pattern}. An allow entry that ` +
103
+ 'cannot be decided does not approve, and a deny or escalate entry that cannot be decided ' +
104
+ 'still applies. Simplify the pattern if this repeats.',
105
+ });
106
+ return 'undecidable';
107
+ }
108
+ return matched ? 'match' : 'no-match';
109
+ }
110
+ /** Compare one candidate string against a string-patterned entry. */
111
+ function comparePattern(matcher, pattern, candidate, foldCase, ctx) {
112
+ if (matcher === 'regexp')
113
+ return testRegexpPattern(pattern, candidate, ctx);
114
+ const left = foldCase ? candidate.toLowerCase() : candidate;
115
+ const right = foldCase ? pattern.toLowerCase() : pattern;
116
+ if (matcher === 'exact')
117
+ return left === right ? 'match' : 'no-match';
118
+ return globToRegExp(right).test(left) ? 'match' : 'no-match';
119
+ }
120
+ /** Fold several candidate results: any match wins; otherwise any undecidable wins. */
121
+ function foldMatches(results) {
122
+ if (results.includes('match'))
123
+ return 'match';
124
+ if (results.includes('undecidable'))
125
+ return 'undecidable';
126
+ return 'no-match';
127
+ }
128
+ /**
129
+ * A `shell` entry against a shell subject.
130
+ *
131
+ * On the **allow** side the command must statically resolve (§3.1) — that check comes FIRST and
132
+ * applies to every matcher, which is what stops a glob spanning a command separator — and the
133
+ * comparison is against the whole normalized command, as written.
134
+ *
135
+ * On the **restrictive** side the entry is compared against the whole normalized command AND
136
+ * against every segment a shell would run (`a; b`, `a && b`, the body of a `$(…)`), with case
137
+ * folded for `exact`/`glob`.
138
+ */
139
+ function matchShellEntry(entry, command, mode, ctx) {
140
+ if (mode === 'allow') {
141
+ // §3.1 — no allow entry of ANY matcher matches a command that does not statically resolve.
142
+ if (classifyCommand(command, normalizeCommand) === null)
143
+ return 'no-match';
144
+ return comparePattern(entry.matcher, entry.pattern, normalizeCommand(command), false, ctx);
145
+ }
146
+ const candidates = [normalizeCommand(command), ...commandSegments(command)];
147
+ const foldCase = entry.matcher !== 'regexp';
148
+ return foldMatches(candidates.map((candidate) => comparePattern(entry.matcher, entry.pattern, candidate, foldCase, ctx)));
149
+ }
150
+ /**
151
+ * §3.1 — a `hint` pattern holds when **every** annotation it names holds effectively (AND within
152
+ * the entry; alternatives are separate entries). Hints it does not name are unconstrained. An empty
153
+ * pattern is a config error, so it cannot reach here as a match-everything.
154
+ */
155
+ function matchHintPattern(pattern, subject, ctx) {
156
+ const effective = ctx.annotations(subject);
157
+ if (!effective)
158
+ return 'undecidable';
159
+ for (const [name, required] of Object.entries(pattern)) {
160
+ if (required === undefined)
161
+ continue;
162
+ if (effective[name] !== required)
163
+ return 'no-match';
164
+ }
165
+ return 'match';
166
+ }
167
+ /** One entry against one subject, in one list's fail-direction. */
168
+ function matchEntry(entry, subject, mode, ctx) {
169
+ if (entry.type === 'shell') {
170
+ // A `shell` entry is about a command, so it says nothing about a tool call. `run_shell_command`
171
+ // is deliberately NOT also matchable as a `tool` subject — see `approvalSubjectFor` in the
172
+ // runner and [[EXT-30]].
173
+ return subject.kind === 'shell'
174
+ ? matchShellEntry(entry, subject.command, mode, ctx)
175
+ : 'no-match';
176
+ }
177
+ if (subject.kind === 'shell')
178
+ return 'no-match';
179
+ if (entry.type !== subject.kind)
180
+ return 'no-match';
181
+ if (entry.type === 'mcpTool' && subject.kind === 'mcpTool') {
182
+ // §3.1 — `*` is the reserved literal meaning every server, and a configured server may not be
183
+ // named it (validated at load).
184
+ if (entry.server !== '*' && entry.server !== subject.server)
185
+ return 'no-match';
186
+ }
187
+ // §4.7.4 — `host`, where present, is an additional exact-match condition. A call with no host
188
+ // never matches an entry that specifies one.
189
+ if (entry.host !== undefined) {
190
+ if (subject.host === undefined || subject.host !== entry.host)
191
+ return 'no-match';
192
+ }
193
+ if (entry.matcher === 'hint') {
194
+ return matchHintPattern(entry.pattern, subject, ctx);
195
+ }
196
+ return comparePattern(entry.matcher, entry.pattern, subject.name, false, ctx);
197
+ }
198
+ /**
199
+ * §3.2 — whether the rater still reviews a call this entry matched. Honored at the rater rungs and
200
+ * inert at the deterministic ones; a per-entry `rate` wins in **both** directions.
201
+ *
202
+ * The default derives from one principle: *an entry skips the rater only to the extent that it
203
+ * recorded what the rater would have seen.* A `shell` + `exact` entry recorded the whole command, so
204
+ * it needs no rating; a pattern recorded a shape, and a tool entry recorded identity (never
205
+ * arguments), so both keep the rater.
206
+ */
207
+ export function approvalEntryRatesCall(entry) {
208
+ if (entry.rate !== undefined)
209
+ return entry.rate;
210
+ return !(entry.type === 'shell' && entry.matcher === 'exact');
211
+ }
212
+ /** The first entry of a list that matches, honoring the list's fail-direction. */
213
+ function firstMatch(entries, subject, mode, ctx) {
214
+ for (const entry of entries) {
215
+ const result = matchEntry(entry, subject, mode, ctx);
216
+ if (result === 'match')
217
+ return entry;
218
+ // §3.1 — an allow match that cannot be decided is NOT a match (escalate); a deny match that
219
+ // cannot be decided IS one (refuse). Escalate follows deny: it fails toward a prompt, which is
220
+ // never an execution.
221
+ if (result === 'undecidable' && mode === 'restrictive')
222
+ return entry;
223
+ }
224
+ return null;
225
+ }
226
+ /**
227
+ * §3.3 — resolve a call against all three lists, most-restrictive-wins.
228
+ *
229
+ * **Every** deny entry is consulted before **any** escalate entry, and every escalate entry before
230
+ * any allow entry. That ordering is the whole property: author order within a list, and the order
231
+ * in which lists were concatenated, cannot change the outcome, and appending an entry can never
232
+ * perturb an existing one. The first match inside a list decides only which entry is reported.
233
+ *
234
+ * Returns `null` when nothing matched — the rung then decides on its own.
235
+ */
236
+ export function resolveApprovalRules(subject, lists, options) {
237
+ const ctx = contextFrom(options);
238
+ const denied = firstMatch(lists.deny, subject, 'restrictive', ctx);
239
+ if (denied)
240
+ return { action: 'deny', entry: denied, rate: false };
241
+ const escalated = firstMatch(lists.escalate, subject, 'restrictive', ctx);
242
+ if (escalated)
243
+ return { action: 'escalate', entry: escalated, rate: false };
244
+ const allowed = firstMatch(lists.allow, subject, 'allow', ctx);
245
+ if (allowed)
246
+ return { action: 'allow', entry: allowed, rate: approvalEntryRatesCall(allowed) };
247
+ return null;
248
+ }
249
+ /**
250
+ * A one-line rendering of an entry, for the provenance a refusal message and an escalation prompt
251
+ * must carry (§3, §3.2) and for the `/approvals` list display.
252
+ *
253
+ * A `shell` + `exact` entry renders as its bare pattern — that is the command the user wrote, and
254
+ * decorating it would only get between them and the line they are looking for. Everything else says
255
+ * what kind of thing it is, because a pattern that is not the command needs to be readable as one.
256
+ */
257
+ export function describeApprovalEntry(entry) {
258
+ const pattern = typeof entry.pattern === 'string' ? entry.pattern : JSON.stringify(entry.pattern);
259
+ const host = entry.type !== 'shell' && entry.host ? ` (host ${entry.host})` : '';
260
+ if (entry.type === 'shell') {
261
+ return entry.matcher === 'exact' ? pattern : `${pattern} (${entry.matcher})`;
262
+ }
263
+ const matcher = entry.matcher === 'exact' ? '' : ` (${entry.matcher})`;
264
+ const subject = entry.type === 'mcpTool' ? `mcpTool ${entry.server}/${pattern}` : `tool ${pattern}`;
265
+ return `${subject}${matcher}${host}`;
266
+ }
267
+ //# sourceMappingURL=matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matcher.js","sourceRoot":"","sources":["../../../src/core/approvals/matcher.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,40 @@
1
+ import type { McpToolApprovalSubject, ToolApprovalSubject } from '#src/core/approvals/matcher.js';
2
+ /**
3
+ * The server identity an MCP tool call gets when its name does not resolve to exactly one
4
+ * configured `mcpServers` key — nothing matched, or two nested keys both did.
5
+ *
6
+ * **The empty string, and that is load-bearing rather than arbitrary.** A server key is
7
+ * `z.string().min(1)` both under `approvals.mcp.servers` (§9) and on an `mcpTool` rule entry
8
+ * (§3.1), so this value is the one identity a user *cannot spell in config*: no entry can name it.
9
+ * An unresolvable call is therefore matchable only by the reserved `server: "*"` (which means every
10
+ * server and is correct here).
11
+ *
12
+ * Being unspellable is what makes it unnameable; it is **not** by itself what makes it untrusted,
13
+ * because a lookup that misses under `servers` falls through to `approvals.mcp.defaults`. That
14
+ * second half is enforced in `trustedAnnotationHints`, which refuses this value outright, and in
15
+ * `mcpDeclaredAnnotationLookup`, which will not look a declaration up under it. Both are required:
16
+ * the first stops a permissive `defaults` reaching an unattributable call, the second stops one
17
+ * being found by string arithmetic on the sentinel.
18
+ */
19
+ export declare const UNRESOLVED_MCP_SERVER = "";
20
+ /** Does this registered tool name sit in the MCP namespace? */
21
+ export declare function isMcpNamespacedToolName(toolName: string): boolean;
22
+ /**
23
+ * The registered tool name mcp-adapters emits for one server's tool — the inverse of
24
+ * {@link approvalSubjectForToolName}'s split, single-sourced so a declared-annotation lookup keyed
25
+ * by registered name round-trips exactly instead of re-deriving the separator.
26
+ */
27
+ export declare function mcpToolRegisteredName(server: string, toolName: string): string;
28
+ /**
29
+ * §4.7.1/§4.7.5 — the subject a gated call on `toolName` presents to the rule matcher.
30
+ *
31
+ * A name outside the MCP namespace is one of ours (a built-in, or a tool the user wired into their
32
+ * own config): `kind: 'tool'`, the trusted provenance. A name inside it is `kind: 'mcpTool'`
33
+ * **always** — with its resolved server where the configured keys name exactly one, and with
34
+ * {@link UNRESOLVED_MCP_SERVER} otherwise.
35
+ *
36
+ * @param toolName The registered tool name, exactly as the model called it.
37
+ * @param configuredMcpServers `Object.keys(config.mcpServers)` — the user's own keys, and the only
38
+ * thing consulted. Nothing a server declares about its own name ever participates (§4.7.5).
39
+ */
40
+ export declare function approvalSubjectForToolName(toolName: string, configuredMcpServers: Iterable<string>): ToolApprovalSubject | McpToolApprovalSubject;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @module core/approvals/mcpSubjects
3
+ *
4
+ * EXT-70 (spec §4.7.5) — **which approval subject a registered tool name presents**.
5
+ *
6
+ * The distinction this module exists to make is the one §4.7.1 rests on: a `tool` subject's
7
+ * annotations are read **verbatim**, an `mcpTool` subject's are believed only per hint and only
8
+ * where the user said so. So a tool that reaches a third-party server while presenting as
9
+ * `kind: 'tool'` is a gate that server can opt itself out of — the exact failure §4.7.1 names.
10
+ * Every name carrying the MCP namespace therefore becomes an `mcpTool` subject, and there is no
11
+ * path back to `kind: 'tool'` for one.
12
+ *
13
+ * ## The server is resolved against the configured keys, never by splitting
14
+ *
15
+ * An MCP tool is named `mcp__<server>__<tool>` (the mcp-adapters convention, single-sourced from
16
+ * {@link MCP_TOOL_NAME_PREFIX}), where `<server>` is the user's own `mcpServers` key — the identity
17
+ * §4.7.5 requires. **A key may itself contain `__`**, so splitting on the separator attributes such
18
+ * a tool to a shorter key that also happens to be configured, and the wrong server means the wrong
19
+ * TRUST lookup: one server's tool wearing another server's belief. Resolution is therefore a scan
20
+ * of the configured key set for the keys that actually prefix this name.
21
+ *
22
+ * Where that does not yield **exactly one** key — no configured server matches, or two nested keys
23
+ * both do — the call resolves to {@link UNRESOLVED_MCP_SERVER}, which is still an `mcpTool`
24
+ * subject. Failing closed to "an MCP tool under a server we cannot name" is the only safe answer;
25
+ * falling back to `kind: 'tool'` would hand an unidentifiable tool the trusted provenance.
26
+ */
27
+ import { MCP_TOOL_NAME_PREFIX } from '#src/constants.js';
28
+ /** The separator mcp-adapters puts between the prefix, the server key and the tool name. */
29
+ const NAME_SEPARATOR = '__';
30
+ /** The `mcp__` namespace every MCP tool name carries. */
31
+ const MCP_NAME_PREFIX = `${MCP_TOOL_NAME_PREFIX}${NAME_SEPARATOR}`;
32
+ /**
33
+ * The server identity an MCP tool call gets when its name does not resolve to exactly one
34
+ * configured `mcpServers` key — nothing matched, or two nested keys both did.
35
+ *
36
+ * **The empty string, and that is load-bearing rather than arbitrary.** A server key is
37
+ * `z.string().min(1)` both under `approvals.mcp.servers` (§9) and on an `mcpTool` rule entry
38
+ * (§3.1), so this value is the one identity a user *cannot spell in config*: no entry can name it.
39
+ * An unresolvable call is therefore matchable only by the reserved `server: "*"` (which means every
40
+ * server and is correct here).
41
+ *
42
+ * Being unspellable is what makes it unnameable; it is **not** by itself what makes it untrusted,
43
+ * because a lookup that misses under `servers` falls through to `approvals.mcp.defaults`. That
44
+ * second half is enforced in `trustedAnnotationHints`, which refuses this value outright, and in
45
+ * `mcpDeclaredAnnotationLookup`, which will not look a declaration up under it. Both are required:
46
+ * the first stops a permissive `defaults` reaching an unattributable call, the second stops one
47
+ * being found by string arithmetic on the sentinel.
48
+ */
49
+ export const UNRESOLVED_MCP_SERVER = '';
50
+ /** Does this registered tool name sit in the MCP namespace? */
51
+ export function isMcpNamespacedToolName(toolName) {
52
+ return toolName.startsWith(MCP_NAME_PREFIX);
53
+ }
54
+ /**
55
+ * The registered tool name mcp-adapters emits for one server's tool — the inverse of
56
+ * {@link approvalSubjectForToolName}'s split, single-sourced so a declared-annotation lookup keyed
57
+ * by registered name round-trips exactly instead of re-deriving the separator.
58
+ */
59
+ export function mcpToolRegisteredName(server, toolName) {
60
+ return `${MCP_NAME_PREFIX}${server}${NAME_SEPARATOR}${toolName}`;
61
+ }
62
+ /**
63
+ * §4.7.1/§4.7.5 — the subject a gated call on `toolName` presents to the rule matcher.
64
+ *
65
+ * A name outside the MCP namespace is one of ours (a built-in, or a tool the user wired into their
66
+ * own config): `kind: 'tool'`, the trusted provenance. A name inside it is `kind: 'mcpTool'`
67
+ * **always** — with its resolved server where the configured keys name exactly one, and with
68
+ * {@link UNRESOLVED_MCP_SERVER} otherwise.
69
+ *
70
+ * @param toolName The registered tool name, exactly as the model called it.
71
+ * @param configuredMcpServers `Object.keys(config.mcpServers)` — the user's own keys, and the only
72
+ * thing consulted. Nothing a server declares about its own name ever participates (§4.7.5).
73
+ */
74
+ export function approvalSubjectForToolName(toolName, configuredMcpServers) {
75
+ if (!isMcpNamespacedToolName(toolName))
76
+ return { kind: 'tool', name: toolName };
77
+ let resolved;
78
+ for (const server of configuredMcpServers) {
79
+ // An empty key is unnameable in `approvals.mcp.servers` and on an `mcpTool` entry, so resolving
80
+ // to it would be indistinguishable from not resolving at all — treat it as the latter.
81
+ if (server === UNRESOLVED_MCP_SERVER)
82
+ continue;
83
+ const prefix = mcpToolRegisteredName(server, '');
84
+ if (!toolName.startsWith(prefix))
85
+ continue;
86
+ const name = toolName.slice(prefix.length);
87
+ if (name.length === 0)
88
+ continue;
89
+ // A second matching key means two nested server names both explain this tool. Neither is more
90
+ // right than the other, so neither is used.
91
+ if (resolved)
92
+ return { kind: 'mcpTool', server: UNRESOLVED_MCP_SERVER, name: toolName };
93
+ resolved = { kind: 'mcpTool', server, name };
94
+ }
95
+ // Nothing configured explains the name: keep the whole name as the tool's identity, since there
96
+ // is no server key to strip and guessing at one is exactly what this function refuses to do.
97
+ return resolved ?? { kind: 'mcpTool', server: UNRESOLVED_MCP_SERVER, name: toolName };
98
+ }
99
+ //# sourceMappingURL=mcpSubjects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcpSubjects.js","sourceRoot":"","sources":["../../../src/core/approvals/mcpSubjects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,4FAA4F;AAC5F,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B,yDAAyD;AACzD,MAAM,eAAe,GAAG,GAAG,oBAAoB,GAAG,cAAc,EAAE,CAAC;AAEnE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC;AAExC,+DAA+D;AAC/D,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,OAAO,QAAQ,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC;AAC9C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAc,EAAE,QAAgB;IACpE,OAAO,GAAG,eAAe,GAAG,MAAM,GAAG,cAAc,GAAG,QAAQ,EAAE,CAAC;AACnE,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,0BAA0B,CACxC,QAAgB,EAChB,oBAAsC;IAEtC,IAAI,CAAC,uBAAuB,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAEhF,IAAI,QAA4C,CAAC;IACjD,KAAK,MAAM,MAAM,IAAI,oBAAoB,EAAE,CAAC;QAC1C,gGAAgG;QAChG,uFAAuF;QACvF,IAAI,MAAM,KAAK,qBAAqB;YAAE,SAAS;QAC/C,MAAM,MAAM,GAAG,qBAAqB,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QACjD,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC;YAAE,SAAS;QAC3C,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAChC,8FAA8F;QAC9F,4CAA4C;QAC5C,IAAI,QAAQ;YAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,qBAAqB,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;QACxF,QAAQ,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAC/C,CAAC;IAED,gGAAgG;IAChG,6FAA6F;IAC7F,OAAO,QAAQ,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,qBAAqB,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AACxF,CAAC"}