@gaunt-sloth/core 2.0.0-alpha.9 → 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
@@ -1,16 +1,181 @@
1
1
  /**
2
2
  * @packageDocumentation
3
3
  * Shell / dev-tools policy: the {@link GthDevToolsConfig} type plus all the resolvers
4
- * that interpret it (shell enablement, timeouts, output budget, allow-list, the EXT-10
5
- * LLM-as-judge gate, and per-command dev-tools selection). Extracted verbatim from the
6
- * former `config.ts` god-file; behaviour is unchanged.
4
+ * that interpret it (shell enablement, timeouts, output budget, per-command dev-tools
5
+ * selection), and since CFG-27 the {@link ApprovalsConfig} value and its resolver
6
+ * {@link resolveApprovals} (the five-rung ladder, the rater's identity profile, and the declared
7
+ * allow/deny lists).
8
+ *
9
+ * It also hosts the GitHub review-tool registry resolvers — {@link isGhReadFileToolEnabled} and
10
+ * {@link getGhReadFileMaxBytes} — which read the same {@link GthConfig.builtInTools} registry
11
+ * through the same per-command-then-root pick as the shell accessors. They live here, in core,
12
+ * rather than beside the tool in `@gaunt-sloth/review` (their only caller), so that one place owns
13
+ * how a `builtInTools` entry is normalised and resolved; a copy sitting next to the tool would be
14
+ * free to drift from the precedence every other tool in the registry obeys.
15
+ *
16
+ * CFG-18 — the dev/shell tools are now configured through the unified {@link GthConfig.builtInTools}
17
+ * registry (`string[] | Record<string, boolean | BuiltInToolConfig>`), NOT the removed per-command
18
+ * `commands.<mode>.devTools` key. {@link GthDevToolsConfig} is therefore no longer an on-disk shape:
19
+ * it is the internal, resolved view that {@link getEffectiveDevToolsConfig} builds from the effective
20
+ * `builtInTools` registry, and that {@link GthDevToolkit} + the shell accessors below consume. This
21
+ * keeps the toolkit/accessor surface stable while the single config surface is `builtInTools`.
7
22
  */
8
- import type { GthCommand } from '#src/core/types.js';
9
- import type { GthConfig, LLMConfig } from '#src/config/types.js';
23
+ import { type GthCommand, StatusLevel } from '#src/core/types.js';
24
+ import type { GthConfig } from '#src/config/types.js';
10
25
  /**
11
- * Config for {@link GthDevToolkit}.
12
- * Tools are not applied when config is not provided.
13
- * Only available in `code`/`exec` mode (and `ask --write`).
26
+ * CFG-18 the per-tool config object carried as a value in the {@link GthConfig.builtInTools}
27
+ * registry (the object form's values, alongside a bare boolean that enables/force-disables a tool).
28
+ * Heterogeneous by tool, modelled as one permissive object (all fields optional) rather than a
29
+ * discriminated union so the registry can carry every tool's shape:
30
+ * - the fixed dev-command tools (`run_tests`/`run_lint`/`run_build`/`run_single_test`) read
31
+ * {@link command} — the shell command to run; its presence enables the tool;
32
+ * - `run_shell_command` reads the EXT-9/12 execution knobs ({@link enabled}/{@link timeout}/
33
+ * {@link maxOutputBytes});
34
+ * - `gth_grep` reads {@link fileSet} (GS2-51) — which corpus to search;
35
+ * - a plain built-in tool (`gth_checklist`, `gth_web_fetch`, …) reads {@link enabled} (or is
36
+ * toggled with a bare boolean in the registry).
37
+ *
38
+ * CFG-26 — the APPROVAL knobs (`allowlist`, `persistAllowlist`, `judge`, `yolo`) are gone from
39
+ * here and live in the top-level {@link ApprovalsConfig}. They were fields of the object shared by
40
+ * EVERY built-in tool, so `gth_grep: { yolo: true }` used to validate; approvals are a property of
41
+ * the session, not of one tool's registry entry.
42
+ */
43
+ export interface BuiltInToolConfig {
44
+ /**
45
+ * Enable / force-disable this tool. For `run_shell_command` the resolution is `enabled ?? default`
46
+ * (EXT-12: default ON in `code` mode, OFF elsewhere), so an object entry WITHOUT `enabled` still
47
+ * defaults ON in `code`; `enabled: false` is the hard escape hatch that disables it even in `code`.
48
+ * For a plain built-in tool, `enabled: false` removes it from the loaded set.
49
+ */
50
+ enabled?: boolean;
51
+ /** The shell command for a fixed dev-command tool (`run_tests`/`run_lint`/`run_build`/`run_single_test`). */
52
+ command?: string;
53
+ /** `run_shell_command`: per-command wall-clock timeout (ms). See {@link SHELL_DEFAULT_TIMEOUT_MS}. */
54
+ timeout?: number;
55
+ /** `run_shell_command`: captured-output byte budget. See {@link SHELL_DEFAULT_MAX_OUTPUT_BYTES}. */
56
+ maxOutputBytes?: number;
57
+ /**
58
+ * `gth_gh_read_file` (CFG-52): ceiling on the DECODED file text the tool returns, in bytes.
59
+ * Over it, the content is truncated at the boundary and carries a marker naming the tool and the
60
+ * cap, so the model knows the file is incomplete. See {@link GH_READ_FILE_DEFAULT_MAX_BYTES}.
61
+ *
62
+ * Named `maxBytes` rather than the shell's `maxOutputBytes` because this is a file read (cf.
63
+ * `read_file`'s byte cap) rather than captured command output.
64
+ */
65
+ maxBytes?: number;
66
+ /**
67
+ * `gth_grep` (GS2-51): which corpus the content-search tool scans, applied consistently to BOTH
68
+ * execution engines (native ripgrep and the in-process JS fallback):
69
+ * - `gitignore` (DEFAULT) — respect `.gitignore`/`.ignore` and skip hidden dot-files. This is the
70
+ * best code-search UX and is already ripgrep's own default, so rg-present machines see NO
71
+ * behaviour change; only the corpus *selection* becomes explicit.
72
+ * - `all` — scan everything except the noise dirs (`node_modules`/`dist`/`.git`/`.idea`); for rg
73
+ * this passes `--no-ignore --hidden`.
74
+ *
75
+ * Example: `{ "builtInTools": { "gth_grep": { "fileSet": "all" } } }`.
76
+ *
77
+ * NOTE: under `gitignore` the JS fallback is a best-effort approximation (skip noise dirs + hidden
78
+ * dot-files); it does NOT parse arbitrary `.gitignore` rules the way rg does. See the residual
79
+ * rg-vs-JS divergence note in `gthGrepTool.ts`.
80
+ */
81
+ fileSet?: 'gitignore' | 'all';
82
+ }
83
+ /**
84
+ * CFG-18 — the widened `builtInTools` setting. Either the legacy `string[]` (each named tool
85
+ * enabled) or a registry keyed by tool name whose values **enable** (`true`), **force-disable**
86
+ * (`false`), or **configure** ({@link BuiltInToolConfig}) each tool. This single key replaces the
87
+ * former split of `builtInTools: string[]` (which tools are on) + per-command `devTools` (how each
88
+ * dev/shell tool is configured).
89
+ *
90
+ * Example — keep the checklist, add web fetch, and configure the shell:
91
+ * ```json
92
+ * { "builtInTools": {
93
+ * "gth_checklist": true,
94
+ * "gth_web_fetch": true,
95
+ * "run_shell_command": { "timeout": 300000 }
96
+ * } }
97
+ * ```
98
+ * Turn the (code-mode default-on) shell OFF: `{ "builtInTools": { "run_shell_command": false } }`.
99
+ */
100
+ export type BuiltInToolsSetting = string[] | Record<string, boolean | BuiltInToolConfig>;
101
+ /**
102
+ * The fixed dev-command tools: each maps a `command` string (from its {@link BuiltInToolConfig})
103
+ * to a run_* tool emitted by {@link GthDevToolkit}.
104
+ */
105
+ export declare const DEV_COMMAND_TOOL_NAMES: readonly ['run_tests', 'run_lint', 'run_build', 'run_single_test'];
106
+ /** The opt-in general-purpose shell tool name. */
107
+ export declare const SHELL_TOOL_NAME = "run_shell_command";
108
+ /**
109
+ * All dev/shell tool names carried in the {@link GthConfig.builtInTools} registry. These are emitted
110
+ * by {@link GthDevToolkit} via the dev-tools bucket, NOT loaded as plain built-in tools — so
111
+ * `getBuiltInTools` skips them (a `run_shell_command` entry in `builtInTools` is legitimate, not an
112
+ * "unknown built-in tool").
113
+ */
114
+ export declare const DEV_TOOL_NAMES: readonly string[];
115
+ /**
116
+ * CFG-52 — the `gh api` file-read tool the review agent gets on a GitHub PR. The tool itself is
117
+ * built in `@gaunt-sloth/review` and stays there: it binds to the PR under review, which the
118
+ * `AVAILABLE_BUILT_IN_TOOLS` factory contract (`tool.get(config)`) cannot supply. Only its NAME
119
+ * lives here, because two packages need it and neither may depend on the other — `review` to decide
120
+ * whether to inject it, and `agent`'s `getBuiltInTools` to SKIP it (see
121
+ * {@link EXTERNALLY_EMITTED_BUILT_IN_TOOL_NAMES}).
122
+ */
123
+ export declare const GH_READ_FILE_TOOL_NAME = "gth_gh_read_file";
124
+ /**
125
+ * Built-in tool names that are legitimate {@link GthConfig.builtInTools} entries but are NOT loaded
126
+ * by `getBuiltInTools`, because the tool is constructed elsewhere: the dev/shell tools come from
127
+ * {@link GthDevToolkit}, and {@link GH_READ_FILE_TOOL_NAME} is built by the review module with the
128
+ * PR context bound in. Without the skip, configuring one of these prints
129
+ * `Unknown built-in tool: <name>` on EVERY command's run — including the ones that never load it.
130
+ */
131
+ export declare const EXTERNALLY_EMITTED_BUILT_IN_TOOL_NAMES: readonly string[];
132
+ /**
133
+ * CFG-52 — default ceiling on the DECODED file text {@link GH_READ_FILE_TOOL_NAME} returns:
134
+ * 600 KiB, roughly 10K lines of code. Deliberately generous, because the tool exists precisely for
135
+ * the case where the diff truncated and the reviewer needs the whole file; a cap that bites in
136
+ * normal use would defeat it. Override per entry with
137
+ * `{ "builtInTools": { "gth_gh_read_file": { "maxBytes": 200000 } } }`.
138
+ */
139
+ export declare const GH_READ_FILE_DEFAULT_MAX_BYTES = 614400;
140
+ /** The commands that can carry {@link GH_READ_FILE_TOOL_NAME}; it is inert everywhere else. */
141
+ export type GhReadFileCommand = 'pr' | 'review';
142
+ /**
143
+ * CFG-52 — whether the review agent gets {@link GH_READ_FILE_TOOL_NAME} on this run.
144
+ *
145
+ * **Opt-OUT: absence means enabled.** The tool is bound to the PR's own head repo and ref, reads
146
+ * nothing local, and no-ops gracefully when `gh` is missing — the content it can reach is the
147
+ * content the review is already about — so making it opt-in would turn it off for every existing
148
+ * `gth pr` user in exchange for a flag most would never find. That is why absence is resolved here
149
+ * rather than through {@link isBuiltInToolEntryEnabled}, which reads an absent entry as OFF.
150
+ *
151
+ * `{ "builtInTools": { "gth_gh_read_file": false } }` turns it off; an object entry configures it
152
+ * (and, like any other tool, `{ "enabled": false }` also disables).
153
+ */
154
+ export declare function isGhReadFileToolEnabled(config: Pick<GthConfig, 'commands' | 'builtInTools'> | undefined, command: GhReadFileCommand): boolean;
155
+ /**
156
+ * CFG-52 — the decoded-text byte ceiling for {@link GH_READ_FILE_TOOL_NAME}, falling back to
157
+ * {@link GH_READ_FILE_DEFAULT_MAX_BYTES}. Only the object form can override it, and an
158
+ * out-of-range / non-numeric value falls back to the default — the same guard
159
+ * {@link getShellMaxOutputBytes} applies.
160
+ */
161
+ export declare function getGhReadFileMaxBytes(config: Pick<GthConfig, 'commands' | 'builtInTools'> | undefined, command: GhReadFileCommand): number;
162
+ /**
163
+ * Normalize the widened {@link BuiltInToolsSetting} to a plain lookup keyed by tool name. The array
164
+ * form maps each name to `true`; the object form passes through unchanged; absent → `{}`.
165
+ */
166
+ export declare function normalizeBuiltInTools(builtInTools: BuiltInToolsSetting | undefined): Record<string, boolean | BuiltInToolConfig>;
167
+ /**
168
+ * Whether a plain built-in tool's registry entry is enabled: a bare `true`, or an object entry that
169
+ * is not `{ enabled: false }` (configuring a tool enables it). A bare `false` force-disables it.
170
+ * Dev/shell tools ({@link DEV_TOOL_NAMES}) are NOT resolved through this — they go through
171
+ * {@link getEffectiveDevToolsConfig} / {@link isShellToolEnabled}.
172
+ */
173
+ export declare function isBuiltInToolEntryEnabled(value: boolean | BuiltInToolConfig | undefined): boolean;
174
+ /**
175
+ * Config for {@link GthDevToolkit} — the INTERNAL, resolved dev/shell view (CFG-18: no longer an
176
+ * on-disk shape; built from the {@link GthConfig.builtInTools} registry by
177
+ * {@link getEffectiveDevToolsConfig}). Tools are not applied when the config is empty. Only active
178
+ * in `code`/`exec` mode (and `ask --write`).
14
179
  */
15
180
  export interface GthDevToolsConfig {
16
181
  /**
@@ -39,21 +204,20 @@ export interface GthDevToolsConfig {
39
204
  /**
40
205
  * Opt-in general-purpose shell tool (`run_shell_command`). Unlike the fixed
41
206
  * `run_*` commands above, this lets the agent run ARBITRARY shell commands it
42
- * composes itself — the agentic-coding escape hatch the deep agent otherwise
43
- * lacks (it can read/write files but not run commands).
207
+ * composes itself — the agentic-coding escape hatch the filesystem tools alone
208
+ * do not give (they read and write files but run nothing).
44
209
  *
45
210
  * EXT-12 — default: ON in `code` mode, OFF elsewhere. When this is ABSENT/undefined,
46
- * `code` mode emits the tool (still GATED behind the per-command approval prompt — the
47
- * absent-config default NEVER implies yolo); `exec` / `ask --write` keep it OFF. An
48
- * EXPLICIT value always wins: `shell: false` (or `{ enabled: false }`) is a hard escape
49
- * hatch that fully disables it even in `code`. Accepts a bare boolean or an
50
- * `{ enabled }` object for symmetry with future per-tool options.
211
+ * `code` mode emits the tool (still GATED behind the approval gate — the absent-config
212
+ * default NEVER implies bypass); `exec` / `ask --write` keep it OFF. An EXPLICIT value
213
+ * always wins: `shell: false` (or `{ enabled: false }`) is a hard escape hatch that fully
214
+ * disables it even in `code`. Accepts a bare boolean or an `{ enabled }` object.
51
215
  *
52
- * Because the model chooses the command, every invocation is gated behind a
53
- * per-command human confirmation dialog (LangChain `humanInTheLoopMiddleware`,
54
- * wired via deepagents' `interruptOn`) UNLESS {@link shellYolo} bypasses it.
55
- * The confirmation not string-filtering is the guardrail, so the command
56
- * is passed through verbatim (pipes / `$` / `;` are all legitimate).
216
+ * Because the model chooses the command, every invocation is gated behind the CFG-26
217
+ * approvals gate (LangChain `humanInTheLoopMiddleware`)
218
+ * UNLESS `approvals.mode: "bypass"` turns the gate off. The gate — not string-filtering — is
219
+ * the guardrail, so the command is passed through verbatim (pipes / `$` / `;` are all
220
+ * legitimate).
57
221
  *
58
222
  * The object form also tunes the EXT-9 Tier-1 hardening applied to every run
59
223
  * (these have safe defaults so bare `shell: true` is already hardened):
@@ -66,61 +230,21 @@ export interface GthDevToolsConfig {
66
230
  *
67
231
  * A hardcoded hardline blocklist of catastrophic commands (rm -rf /, mkfs, dd
68
232
  * to a block device, fork bomb, shutdown/reboot, …) is refused even under
69
- * {@link shellYolo}; that floor is not configurable.
70
- *
71
- * Example: `{ "shell": true }`,
72
- * `{ "shell": { "enabled": true, "timeout": 300000, "maxOutputBytes": 200000 } }`.
233
+ * `approvals.mode: "bypass"`; that floor is not configurable.
73
234
  *
74
- * The object form additionally accepts EXT-9 Tier-2 allow-list knobs:
75
- * - `allowlist`: master switch for the scoped approval allow-list (session +
76
- * persisted `always`). Default `true` once a command is approved at `session`/
77
- * `always` scope, flag-variants of the same classified operation auto-approve
78
- * without re-prompting. Set `false` to require fresh approval for every command.
79
- * - `persistAllowlist`: whether `always`-scoped approvals are written to the project
80
- * allow-list file (`.gsloth/.gsloth-settings/shell-allowlist.json`). Default `true`.
81
- * When `false`, an `always` decision behaves like `session` (in-memory only).
235
+ * On-disk (CFG-18) these live on the `run_shell_command` entry of `builtInTools`, e.g.
236
+ * `{ "builtInTools": { "run_shell_command": true } }` or
237
+ * `{ "builtInTools": { "run_shell_command": { "timeout": 300000, "maxOutputBytes": 200000 } } }`.
82
238
  *
83
- * The object form also accepts the EXT-10 LLM-as-judge safety gate (default OFF):
84
- * - `judge`: an opt-in, tiered auto-approve pre-filter that vets each `run_shell_command`
85
- * with a lightweight judge model BEFORE the human prompt. It auto-approves clearly-safe
86
- * commands (fatigue reducer), escalates the rest to the existing human prompt, and may
87
- * reject clearly-catastrophic ones. Default OFF because it costs one LLM call per command.
88
- * Accepts a bare boolean (`judge: true` → defaults: auto-approve low, escalate medium/high,
89
- * judge model = `config.llm`) or an object:
90
- * - `enabled`: turn the gate on.
91
- * - `autoApproveLow`: auto-approve `low`-risk, statically-resolvable commands. Default true.
92
- * - `blockHigh`: reject clearly-catastrophic (`high` + destructive) verdicts WITHOUT
93
- * prompting. Default false (conservative; EXT-9's hardline floor already refuses truly
94
- * catastrophic commands at exec time).
95
- * - `model`: an optional separate (e.g. cheaper) judge model config. Defaults to `config.llm`.
96
- * Hardening (always on when the judge runs): the command is normalized + XML-tagged as
97
- * UNTRUSTED input in the judge prompt; a judge throw/timeout/parse-failure fails CLOSED
98
- * (escalate, never auto-approve); commands whose target can't be statically resolved
99
- * (shell composition / substitution / redirection) and interpreter+script invocations that
100
- * leak ALL_CAPS env vars are NEVER auto-approved.
239
+ * CFG-26 the approval knobs that used to live here (`allowlist`, `persistAllowlist`,
240
+ * `judge`, `yolo`) moved to the top-level `approvals` block ({@link ApprovalsConfig}); read
241
+ * them through {@link resolveApprovals}, never from this object.
101
242
  */
102
243
  shell?: boolean | {
103
244
  enabled?: boolean;
104
245
  timeout?: number;
105
246
  maxOutputBytes?: number;
106
- allowlist?: boolean;
107
- persistAllowlist?: boolean;
108
- judge?: boolean | {
109
- enabled?: boolean;
110
- autoApproveLow?: boolean;
111
- blockHigh?: boolean;
112
- model?: LLMConfig;
113
- };
114
247
  };
115
- /**
116
- * Opt-out of the per-command confirmation dialog for {@link shell}
117
- * (`run_shell_command`) — the explicit "yolo" bypass. When `true` AND `shell`
118
- * is enabled, the shell tool runs without any approval interrupt: the model's
119
- * commands execute immediately. Dangerous by design; off by default.
120
- *
121
- * Example: `{ "shell": true, "shellYolo": true }`.
122
- */
123
- shellYolo?: boolean;
124
248
  }
125
249
  /**
126
250
  * Default per-command shell timeout (ms) when {@link GthDevToolsConfig.shell}
@@ -136,17 +260,19 @@ export declare const SHELL_DEFAULT_TIMEOUT_MS = 120000;
136
260
  export declare const SHELL_DEFAULT_MAX_OUTPUT_BYTES = 100000;
137
261
  /**
138
262
  * Normalize the {@link GthDevToolsConfig.shell} opt-in (bare boolean or
139
- * `{ enabled }`) to a plain boolean. Centralized so the toolkit (tool emission)
140
- * and the deep agent (interrupt wiring) agree on what "shell enabled" means.
141
- *
142
- * EXT-12 — default-resolution: an EXPLICIT value always wins (a bare boolean, or the
143
- * object form's `enabled`), so `shell: false` / `{ enabled: false }` remains a hard
144
- * escape hatch that fully disables the tool. Only when `shell` is ABSENT/undefined does
145
- * the per-mode default apply: in `code` mode the shell tool is ON by default (still
146
- * gated the per-command approval interrupt is wired separately and is NOT bypassed by
147
- * this), and OFF everywhere else (`exec`, `ask --write`, …) to preserve prior behaviour.
148
- * The default is `code`-mode only because `code` is the interactive agentic-coding surface
149
- * where a TTY can answer the approval prompt; the absent-config default never implies yolo.
263
+ * `{ enabled }`) to a plain boolean. Centralized so tool emission and interrupt
264
+ * wiring agree on what "shell enabled" means.
265
+ *
266
+ * EXT-12 / CFG-18 — default-resolution is `enabled ?? default`. An EXPLICIT `enabled` always wins
267
+ * (a bare boolean, or the object form's `enabled`), so `shell: false` / `{ enabled: false }` remains
268
+ * a hard escape hatch that fully disables the tool. When `enabled` is ABSENT — whether `shell` is
269
+ * undefined OR an object that omits `enabled` (e.g. `{ timeout: 300000 }`, i.e. a
270
+ * `{ "run_shell_command": { "timeout": 300000 } }` registry entry) the per-mode default applies:
271
+ * ON in `code` mode (still gated the per-command approval interrupt is wired separately and is NOT
272
+ * bypassed by this), OFF everywhere else (`exec`, `ask --write`, …). This is the CFG-18 change from
273
+ * the old `enabled === true` object semantics: configuring the shell no longer silently turns it off.
274
+ * The default is `code`-mode only because `code` is the interactive agentic-coding surface where a
275
+ * TTY can answer the approval prompt; the absent-config default never implies yolo.
150
276
  *
151
277
  * @param command The active command, so the absent-config default can be scoped to `code`.
152
278
  * Omit (or pass a non-`code` command) to keep the historical OFF-by-default behaviour.
@@ -165,48 +291,710 @@ export declare function getShellTimeoutMs(devTools: GthDevToolsConfig | undefine
165
291
  */
166
292
  export declare function getShellMaxOutputBytes(devTools: GthDevToolsConfig | undefined): number;
167
293
  /**
168
- * Whether the EXT-9 Tier-2 scoped allow-list is active. Default `true`; only the object
169
- * form's `allowlist: false` disables it (a bare `shell: true` keeps it on). When off, the
170
- * runner prompts for every `run_shell_command` regardless of prior approvals.
294
+ * Resolve the {@link GthDevToolsConfig} that applies to the active command from the unified
295
+ * {@link GthConfig.builtInTools} registry (CFG-18 replaces the removed per-command `devTools`).
296
+ * Mirrors the per-command selection used by `builtInToolsConfig.getDefaultTools`: `exec`
297
+ * `commands.exec`, `ask --write` → `commands.ask`, `code` → `commands.code`; `undefined` elsewhere
298
+ * (the dev/shell tools are inert there). The effective registry for the scope is the per-command
299
+ * `builtInTools` if set, else the root `builtInTools` — matching `getEffectiveConfig`'s replace
300
+ * merge. Shared in core so the runner's allow-list/judge gates stay in lockstep with where the
301
+ * shell tool is actually emitted.
302
+ */
303
+ export declare function getEffectiveDevToolsConfig(config: Pick<GthConfig, 'commands' | 'builtInTools' | 'askWriteMode'> | undefined, command: GthCommand | undefined): GthDevToolsConfig | undefined;
304
+ /**
305
+ * CFG-27 (spec §1, §2) — **the ladder**. There is ONE approvals setting and it is a single ordered
306
+ * ladder; each rung fully determines behaviour. There are no severity thresholds, no strictness
307
+ * levels and no independent rater on/off switch.
308
+ *
309
+ * | # | Rung | Rater | LLM cost |
310
+ * |---|---|---|---|
311
+ * | 1 | `manual` | no | none |
312
+ * | 2 | `write` | no | none |
313
+ * | 3 | `assisted` | yes | 1 call per gated call |
314
+ * | 4 | `auto` | yes | 1–2 calls per gated call |
315
+ * | 5 | `bypass` | no | none |
316
+ *
317
+ * Rungs 1, 2 and 5 are fully deterministic: no model is consulted, so behaviour is reproducible
318
+ * and costs nothing.
319
+ *
320
+ * **CFG-39 — four postures plus one modifier, not five peers.** `write` is not a rung on a trust
321
+ * ladder: it is the same posture as `manual` with a different auto-granted set. The four postures
322
+ * a user chooses between are {@link APPROVAL_POSTURES} (`manual` → `assisted` → `auto`, plus
323
+ * `bypass`), and that ordering is legible in the names themselves. `write` remains fully settable
324
+ * — via `/approvals write` and via config — and simply does not occupy a row in quick access.
325
+ * This constant keeps ALL FIVE members: it is the type's domain, the set
326
+ * {@link resolveInterruptToolNames} unions over, and what makes `write` settable at all.
327
+ *
328
+ * **`bypass` is NOT a higher-autonomy rung than `auto`** (§2.5). Both let the agent act
329
+ * without asking; `bypass` is the same autonomy with the checks removed. The ordering below is the
330
+ * order the rungs are *offered* in, and must never be presented as though `auto` were an
331
+ * incomplete `bypass`.
332
+ *
333
+ * Identifiers are lower-case single words (§9.1) because the same token must work as a config
334
+ * value, a slash-command argument and a CLI flag — a space breaks the last two. Display names are
335
+ * capitalised; see {@link APPROVAL_RUNG_LABELS}.
336
+ */
337
+ export declare const APPROVAL_RUNGS: readonly ['manual', 'write', 'assisted', 'auto', 'bypass'];
338
+ /** One rung of {@link APPROVAL_RUNGS}. */
339
+ export type ApprovalRung = (typeof APPROVAL_RUNGS)[number];
340
+ /**
341
+ * §9.1 rule / §10 rule 4 — the display spelling of each rung, with spaces. An identifier and a
342
+ * label do not have to match and only one of them has to survive a shell, so user-facing prose
343
+ * uses these and never the kebab-case identifiers.
171
344
  */
172
- export declare function isShellAllowlistEnabled(devTools: GthDevToolsConfig | undefined): boolean;
345
+ export declare const APPROVAL_RUNG_LABELS: Record<ApprovalRung, string>;
173
346
  /**
174
- * Whether `always`-scoped approvals are persisted to the project allow-list file. Default
175
- * `true`; only the object form's `persistAllowlist: false` disables persistence (an
176
- * `always` decision then behaves as `session`).
347
+ * CFG-39 **the four postures**, in the order they are offered: the rows `/approvals` presents
348
+ * when it asks the user to choose one.
349
+ *
350
+ * `write` is deliberately absent, and its absence is the whole point of the reframe. It is not a
351
+ * rung on a trust ladder — it is `manual`'s posture with a different auto-granted set, so listing
352
+ * it as a fifth peer is what made a four-point ladder read as five indistinguishable ones. It stays
353
+ * fully settable via `/approvals write` and via config; it simply leaves quick access.
354
+ *
355
+ * **This is a presentation list, never a policy one.** Every predicate, every `Record<ApprovalRung,
356
+ * …>` and the interrupt set are built from {@link APPROVAL_RUNGS}, which keeps all five. Using this
357
+ * constant to decide behaviour would silently un-settle `write`.
177
358
  */
178
- export declare function isShellAllowlistPersisted(devTools: GthDevToolsConfig | undefined): boolean;
359
+ export declare const APPROVAL_POSTURES: readonly ["manual", "assisted", "auto", "bypass"];
179
360
  /**
180
- * Resolved settings for the EXT-10 LLM-as-judge safety gate.
361
+ * CFG-39 the one-line note that `write` exists, shown as picker CHROME beside the `manual` row.
362
+ *
363
+ * It lives here rather than appended to {@link APPROVAL_RUNG_DESCRIPTIONS}`.manual` on purpose: the
364
+ * descriptions are one surface's copy shared by six, so a sentence about quick-access mechanics
365
+ * would follow the mode into the status display and the tool-description layer, where it means
366
+ * nothing.
181
367
  */
182
- export interface ShellJudgeSettings {
183
- /** Whether the judge gate runs at all. */
184
- enabled: boolean;
185
- /** Auto-approve `low`-risk, statically-resolvable commands (the fatigue reducer). */
186
- autoApproveLow: boolean;
187
- /** Reject clearly-catastrophic (`high` + destructive) verdicts without prompting. */
188
- blockHigh: boolean;
189
- /** Optional separate judge model config; when absent the runner uses `config.llm`. */
190
- model?: LLMConfig;
368
+ export declare const APPROVAL_WRITE_MODIFIER_HINT: string;
369
+ /**
370
+ * §10 — what each mode is **for**, in at most two sentences, shown wherever a mode is chosen or
371
+ * displayed.
372
+ *
373
+ * **The first sentence is load-bearing and must stand alone.** The `/approvals` picker, the text
374
+ * fallback and the usage hint all render one line per mode through `firstSentence`, so sentence
375
+ * one has to answer "what is this mode for" on its own and sentence two carries the qualification.
376
+ * Keep it short enough to read as a menu row.
377
+ *
378
+ * **Say what the mode is for, not only what it permits.** A description that lists permissions
379
+ * invites the category error these modes actually suffer: Manual reads as "the safe one", so it
380
+ * gets picked for a long unattended run — where the deciding is done by a human, and a human is the
381
+ * fastest-degrading decider in the system. Manual and Write are bounded-volume tools and their copy
382
+ * says so.
383
+ *
384
+ * The wording is constrained by four normative rules (state what the mode PERMITS, state the
385
+ * allow-list carve-out, never claim safety this system cannot deliver, use the display spelling)
386
+ * plus §8.1 — the hardline floor is real but is NEVER advertised, so descriptions cite only
387
+ * protections the user can inspect and extend, i.e. the deny list. Six further constraints bind
388
+ * every edit here:
389
+ *
390
+ * 1. **No description may imply containment.** The gate protects against accidents, not intent, and
391
+ * a working-folder claim collapses the moment the agent has a shell — `write_file` refuses a
392
+ * path that `touch` then writes. The narrow true form (the built-in file *tools* are confined)
393
+ * is stated once, on `write`, alongside the fact that the shell is not confined that way.
394
+ * 2. **Claims are scoped to the session the user is in — ratified by Andrew on 2026-08-13** as the
395
+ * standing rule for every in-product approval promise ([[CFG-40]]), so it is a decision and not
396
+ * a habit inherited from whichever string happened to be written last. What makes it true
397
+ * rather than merely convenient: these strings render on terminal surfaces only, and every
398
+ * surface that renders them is a session a person is sitting in, so a claim scoped to *this
399
+ * session* describes the only place the sentence appears rather than hedging around a
400
+ * falsehood. Unscoped it would not: a sentence whose subject is "Gaunt Sloth" and whose claim
401
+ * is that it always asks is false over the AG-UI server, which drives the agent itself and
402
+ * drains no approval interrupt ([[EXT-54]] — worth fixing on its own merits, never as a gate on
403
+ * this copy).
404
+ *
405
+ * **The ACP server does ask, and that does not re-open this rule.** It drives
406
+ * `GthAgentRunner`, registers a per-turn tool-approval callback and raises
407
+ * `session/request_permission`, so a human there really is asked. What it puts in front of them
408
+ * is assembled from the rater's verdict, the matched `approvals.escalate` entry and the grant
409
+ * preview — never from `APPROVAL_RUNG_DESCRIPTIONS` or any other posture copy — so no string
410
+ * governed here renders on a server surface, and the ruling's conclusion is untouched.
411
+ *
412
+ * **That is also the boundary: the moment an approvals string is rendered by a server surface
413
+ * this ruling stops covering it**, and the scoped sentence has to be re-earned there rather
414
+ * than inherited from here.
415
+ * 3. **`assisted` MUST keep the sentence saying files are still rewritten and deleted without
416
+ * asking** — it sounds safer than it is, and that clause is the correction.
417
+ * 4. **A qualification may not live in the second sentence alone.** The picker, the text fallback
418
+ * and the usage hint all render `firstSentence` and nothing else, so the opener is the whole
419
+ * message on the three surfaces a user reads while *choosing* a mode. An opener that sells a
420
+ * behavioural difference the product does not have is not rescued by a sentence two those
421
+ * surfaces never print — check a wording by rendering it, not by reading the constant.
422
+ * 5. **`auto` is the mode a reader most wants to hear is quiet, and it is not.** It settles some
423
+ * risky commands with the rater instead of interrupting — that difference is real and may be
424
+ * stated — but the copy MUST also say that a bounded exchange ends at the user, and MUST NOT
425
+ * promise the user watches it happen. The rounds reach a person at the escalation, all of them
426
+ * at once, and nothing renders them before that ([[TUI-C26]]), so a sentence implying a live
427
+ * commentary would be describing a screen that does not exist.
428
+ * 6. **Every rated rung states a LIMIT ON USE in its opener, and `recoverable` is the word that
429
+ * carries it** ([[CFG-48]]). A rated rung settles some risky commands without asking anyone, so
430
+ * the sentence offering it has to say what kind of work that is acceptable for — and a
431
+ * *terminus* is not a limit on use. "then asks you" and "anything riskier comes to you" say
432
+ * where the exchange ends; they do not say when the mode is a bad idea, which is what the three
433
+ * UNRATED rungs each say ("not a mode to leave running", "a bounded stretch", "a throwaway
434
+ * environment you would not mind losing"). Constraint 4 is why it must be the opener: the three
435
+ * surfaces a user chooses from print `firstSentence` and nothing else.
436
+ *
437
+ * **"Unrated", not "deterministic", and the distinction is this file's own.** The complement of
438
+ * {@link isRatedRung} is `{manual, write, bypass}` — the rungs that consult no rater. That is NOT
439
+ * the set {@link isDeterministicRung} names, which is `{manual, write}`: `bypass` gates nothing at
440
+ * all, so it is not a rung that decides deterministically, it is the absence of a decision. Prose
441
+ * elsewhere in the repo calls the three "deterministic"; against the predicate exported a hundred
442
+ * lines below, that reading is wrong, so this constraint says "unrated" and means the complement.
443
+ *
444
+ * **A lexical test watches the word, and its failure is not a false positive.** "Carries a
445
+ * limit-on-use clause" has no mechanical predicate, so the rule names its own carrier token
446
+ * instead: `packages/app/spec/tui/slashCommands.spec.ts` — *"every rated rung's opener says what
447
+ * kind of work it is for, and only the rated ones do"* — takes the rated set from
448
+ * {@link isRatedRung} rather than a literal pair, so a sixth rated rung is required to carry a
449
+ * clause on the day it is added, and asserts the three unrated rungs do NOT carry the token, so
450
+ * that spreading `recoverable` across all five cannot satisfy it. Rewriting this copy is fine;
451
+ * dropping the word is the thing that must not happen quietly, because that is exactly how
452
+ * Auto's cautionary clause was lost once already — in an ordinary rewrite that broke no test.
453
+ *
454
+ * Everything these two sentences cannot hold lives at {@link APPROVAL_PROTECTION_DOCS_URL}, which
455
+ * the surfaces print beside the copy rather than each description repeating it.
456
+ */
457
+ export declare const APPROVAL_RUNG_DESCRIPTIONS: Record<ApprovalRung, string>;
458
+ /**
459
+ * The page that carries what these modes do and do NOT protect you from — the reasoning the
460
+ * two-sentence descriptions deliberately do not hold.
461
+ *
462
+ * **A GitHub blob URL, matching the one other user-facing runtime doc link in this package** (the
463
+ * 2.0 migration pointer in `config/schema.ts`). The docs site publishes the same page at
464
+ * `https://gauntsloth.app/docs/guides/what-approvals-protect-you-from/`; move this constant there
465
+ * once that path serves the page, and nothing else changes.
466
+ */
467
+ export declare const APPROVAL_PROTECTION_DOCS_URL = "https://github.com/pukeko-robotics/gaunt-sloth/blob/main/docs/guides/what-approvals-protect-you-from.md";
468
+ /**
469
+ * The docs pointer as the surfaces print it: **label and URL as two separate lines**, never one
470
+ * joined string. Every notice surface takes `lines: string[]` and renders one line each, so a bare
471
+ * URL on its own line is the only form that survives a narrow pane without the break landing
472
+ * mid-path — and it is what lets a terminal that linkifies URLs pick the whole thing up.
473
+ *
474
+ * It lives beside the descriptions rather than inside them for {@link APPROVAL_WRITE_MODIFIER_HINT}'s
475
+ * reason: one line about where to read more, repeated into all five descriptions, would follow each
476
+ * mode into the tool-description layer and the status display, where it is noise.
477
+ */
478
+ export declare const APPROVAL_PROTECTION_DOCS_LINES: readonly string[];
479
+ /** Narrowing type guard for a raw string that may name a rung. */
480
+ export declare function isApprovalRung(value: unknown): value is ApprovalRung;
481
+ /** The rungs at which every gated call is rated by the model (§2.3, §2.4). */
482
+ export declare function isRatedRung(rung: ApprovalRung): boolean;
483
+ /**
484
+ * [[EXT-29]] (§5) — the rung at which a `destructive` rating opens a **negotiation** with the rater
485
+ * instead of going to the human: the agent may revise the command or justify it, the rater re-rates
486
+ * with the exchange in view, and only a spent bound reaches a person.
487
+ *
488
+ * **This is the one predicate that separates `auto` from `assisted`, and it is deliberately ONE.**
489
+ * Three places have to agree about it — the decision mapping
490
+ * ({@link import('../core/shell/rater.js').mapVerdictToAction}, which returns `reject` here and
491
+ * `escalate` at `assisted`), the rating prompt (§5.2's wording rules are addressed to the agent, so
492
+ * they are turned on by this and not by whether a transcript happens to exist yet), and the runner
493
+ * that counts the rounds. Two of them agreeing and the third not is exactly how the two rated rungs
494
+ * would drift back into being the same posture with different names.
495
+ *
496
+ * Written as a `=== 'auto'` test rather than as "rated but not assisted" so that a sixth rung has to
497
+ * be classified deliberately rather than inheriting a negotiation by omission.
498
+ */
499
+ export declare function isNegotiatingRung(rung: ApprovalRung): boolean;
500
+ /**
501
+ * The rungs that decide a gated call **without a model** — `manual` and `write` (§2.1, §2.2).
502
+ * Everything they do not auto-grant goes to the human, so these are the two rungs a user picks in
503
+ * order to read and approve every tool call themselves.
504
+ *
505
+ * The complement of {@link isRatedRung} plus `bypass`; written out rather than negated so that a
506
+ * sixth rung would have to be classified deliberately instead of defaulting into this set.
507
+ */
508
+ export declare function isDeterministicRung(rung: ApprovalRung): boolean;
509
+ /**
510
+ * **The one rule: does `rung` gate this tool — i.e. must this call be decided rather than simply
511
+ * run?** Everything else in this area is a projection of this predicate over a set of names.
512
+ *
513
+ * - The shell is gated whenever the shell gate is on, at EVERY rung (`bypass` included, so §2.5's
514
+ * deny list can still fire — see the `bypass` arm of `GthAgentRunner.decideToolApproval`).
515
+ * - At the two **deterministic** rungs, a tool is gated when the rung's own grant does not cover its
516
+ * access class ({@link isAccessClassGrantedAtRung}). At `manual` that leaves only the built-in
517
+ * READ tools free; at `write`, the built-in read and write tools. The write built-ins, the shell,
518
+ * MCP tools and custom/agent-authored tools all escalate to the human.
519
+ * - At `assisted`, `auto` and `bypass` nothing but the shell is gated. **That split is
520
+ * deliberate and load-bearing, not tidiness.** At a rated rung a gated non-shell call reaches the
521
+ * `subject.kind !== 'shell'` arm of `GthAgentRunner.decideToolApproval`, which floors it at
522
+ * `destructive` and sends it to the human *with no rating call*, because §4.3 keeps the rater on
523
+ * the shell until [[EXT-30]]. Gating there would silently turn every MCP call at `assisted` into
524
+ * a human prompt — a UX change belonging to EXT-30, not to the two rungs whose published
525
+ * descriptions this predicate makes true.
526
+ *
527
+ * `gateShell` only ever WIDENS the result. At a deterministic rung the shell is gated by its own
528
+ * (absent) access class if it is bound at all, so `gateShell: false` does not exempt it — an
529
+ * exemption keyed to one tool NAME is the defect class this predicate exists to remove. In practice
530
+ * a disabled shell tool is never bound, so the two agree.
531
+ *
532
+ * **This takes no bound toolset**, which is what lets `GthAgentRunner` ask it about a single
533
+ * arriving call: the runner sees only the names the graph registered, and a graph builder that
534
+ * registers tools of its own leaves them off that list. A decision that consulted a bound list
535
+ * would grant such a tool at `manual` purely because the runner could not see it.
536
+ */
537
+ export declare function isToolGatedAtRung(options: {
538
+ toolName: string;
539
+ rung: ApprovalRung;
540
+ gateShell: boolean;
541
+ }): boolean;
542
+ /**
543
+ * **The LIVE gated set: which bound tools the rung in force actually gates.** What a decision is
544
+ * measured against — the tool descriptions the model reads (§4.5) and the rater's granted-tools
545
+ * summary (§4.4) are both built from this, so neither can tell the model a tool is free while the
546
+ * gate escalates it.
547
+ *
548
+ * **It is NOT what the backends wire into the interrupt.** That is
549
+ * {@link resolveInterruptToolNames}, and the two are different sets on purpose: the interrupt is
550
+ * installed once, at agent init, while `/approvals <rung>` moves the rung underneath it for the rest
551
+ * of the session. A set that carried the rung would be frozen at the rung the session started on —
552
+ * and since the default is `assisted`, typing `/approvals manual` would leave exactly the write
553
+ * tools this design escalates ungated. So the interrupt is wired rung-independently and
554
+ * `GthAgentRunner.decideToolApproval` consults {@link isToolGatedAtRung} against the LIVE rung.
555
+ *
556
+ * **Derived from the bound toolset, never a hand-written list.** A static list of built-ins would
557
+ * leave MCP, custom and agent-authored tools out — the exact tools with no access class and so the
558
+ * exact tools the deterministic rungs must escalate. `boundToolNames` must therefore be the FINAL
559
+ * toolset the graph is handed, including any tool the graph builder registers itself, which by
560
+ * definition never appears in the array gsloth passes it.
561
+ *
562
+ * Order is stable: the shell first, then bound order. Duplicates are collapsed, so a caller may pass
563
+ * overlapping name sources without deduplicating first.
564
+ */
565
+ export declare function resolveGatedToolNames(options: {
566
+ rung: ApprovalRung;
567
+ gateShell: boolean;
568
+ boundToolNames: readonly string[];
569
+ }): readonly string[];
570
+ /**
571
+ * {@link COMMAND_ANSWERS_APPROVALS} as a predicate. An unset command is a session driven by
572
+ * `GthAgentRunner` (nothing else leaves it unset), so it answers approvals.
573
+ *
574
+ * **`?? true` is the fail-safe default, not defensive noise.** The lookup yields `undefined` for a
575
+ * value outside {@link GthCommand}, and `undefined` is falsy — which would tell the caller to
576
+ * install NO approval interrupt, the one direction this predicate must never fail in. TypeScript
577
+ * makes that unreachable from inside this repo, but the function is re-exported from the public
578
+ * `@gaunt-sloth/core/config.js` barrel, so an untyped consumer can reach it. The coalesce restores
579
+ * runtime totality without weakening the compile-time totality the record already gives.
580
+ */
581
+ export declare function commandAnswersApprovals(command: GthCommand | undefined): boolean;
582
+ /**
583
+ * **The interrupt set: which tool names the backends wire into the approval interrupt.** Every bound
584
+ * tool that ANY rung could gate — the union of {@link resolveGatedToolNames} over
585
+ * {@link APPROVAL_RUNGS}, which in practice is the shell plus every bound tool that is not a
586
+ * built-in READ tool.
587
+ *
588
+ * One derivation for every backend, for the same reason {@link resolveShellApprovalGate} is one: a
589
+ * set computed twice is a set that drifts.
590
+ *
591
+ * **Deliberately rung-independent.** The interrupt is installed once, when the agent is built, and
592
+ * `/approvals <rung>` then moves the rung for the rest of the session without rebuilding it. Only a
593
+ * set that covers every rung can survive that: the interrupt fires and
594
+ * `GthAgentRunner.decideToolApproval` decides on the rung in force, which is where the rung has
595
+ * always been read. **Wiring wider does not gate wider** — a call the live rung does not gate is
596
+ * approved there with no rating call and no prompt, so `assisted`, `auto` and `bypass` behave
597
+ * exactly as they do when the interrupt holds the shell alone.
598
+ *
599
+ * **Only for a command that answers approvals** ({@link commandAnswersApprovals}). A surface that
600
+ * drains nothing gets the shell-gate set instead, because a rung-independent set there parks tool
601
+ * calls nobody can reach.
602
+ */
603
+ export declare function resolveInterruptToolNames(options: {
604
+ gateShell: boolean;
605
+ boundToolNames: readonly string[];
606
+ }): readonly string[];
607
+ /**
608
+ * EXT-71 §3.1 — the **subject** axis of a rule entry, and only that. The schema twin is
609
+ * `APPROVAL_ENTRY_TYPES` in `config/schema.ts`. What holds the two together is
610
+ * `approvalEntrySchema.spec.ts`, where a list of `ApprovalEntry`-typed literals is parsed by that
611
+ * schema: a value either side stops accepting fails there. That is a weaker pin than a direct
612
+ * equality assertion — it catches a narrowing, not a widening on one side alone.
613
+ */
614
+ export type ApprovalEntryType = 'shell' | 'tool' | 'mcpTool';
615
+ /** EXT-71 §3.1 — the **comparison** axis of a rule entry, and only that. */
616
+ export type ApprovalMatcher = 'exact' | 'glob' | 'regexp' | 'hint';
617
+ /**
618
+ * §4.7 — the four MCP `ToolAnnotations` hint names, and the whole vocabulary. It is the same list
619
+ * on both sides of the design: what a `hint` pattern may name ({@link ApprovalHintPattern}) and what
620
+ * a user may believe from a server ({@link McpServerApprovalsConfig.trustAnnotations}).
621
+ *
622
+ * **The schema twin `HINT_ANNOTATION_KEYS` in `config/schema.ts` is a deliberate duplicate, and the
623
+ * reason is layering, not oversight.** Neither file may import the other. `schema.ts` states in its
624
+ * own header that it must stay pure and cwd/fs-independent because it feeds `z.toJSONSchema()`, and
625
+ * importing this module would pull `core/types.js` and the whole runtime policy surface into it;
626
+ * importing `schema.ts` here would in turn pull zod into every module that only wanted a policy
627
+ * type. So the vocabulary is written once per layer on purpose — do not "simplify" it by making one
628
+ * import the other.
629
+ *
630
+ * What keeps the two honest instead is the equality assertion in `mcpApprovalsBlock.spec.ts`, which
631
+ * fails the moment they drift. Drift matters in one direction especially: a name the config accepts
632
+ * but the derivation never reads fails silently, and it fails toward trusting. Change one list,
633
+ * change the other.
634
+ */
635
+ export declare const TOOL_ANNOTATION_HINTS: readonly ['readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint'];
636
+ /** One of {@link TOOL_ANNOTATION_HINTS}. */
637
+ export type ToolAnnotationHint = (typeof TOOL_ANNOTATION_HINTS)[number];
638
+ /**
639
+ * EXT-71 §3.1 — a `hint` pattern: the four MCP `ToolAnnotations` booleans, each mapped to the
640
+ * value it must EFFECTIVELY hold (§4.7.1). All named hints must match (AND within the entry);
641
+ * hints not named are unconstrained; `false` is the spelling of negation. At least one must be
642
+ * named — an empty object is a config error, never a match-everything.
643
+ */
644
+ export interface ApprovalHintPattern {
645
+ readOnlyHint?: boolean;
646
+ destructiveHint?: boolean;
647
+ idempotentHint?: boolean;
648
+ openWorldHint?: boolean;
649
+ }
650
+ /** Fields every rule entry may carry, whatever its subject. */
651
+ interface ApprovalEntryCommon {
652
+ /**
653
+ * §3.2 — whether the rater still reviews a call this entry matched, honored at the rater rungs
654
+ * and inert at the deterministic ones. Valid on EVERY type. Absent takes the §3.2 default: an
655
+ * entry skips the rater only to the extent that it recorded what the rater would have seen, so
656
+ * `shell` + `exact` defaults to `false` and everything else to `true`.
657
+ */
658
+ rate?: boolean;
659
+ }
660
+ /** §3.1 — a `shell` entry: a command, compared against the normalized command string. */
661
+ export interface ShellApprovalEntry extends ApprovalEntryCommon {
662
+ type: 'shell';
663
+ /** `hint` is absent on purpose — a command carries no tool annotations. */
664
+ matcher: 'exact' | 'glob' | 'regexp';
665
+ pattern: string;
666
+ }
667
+ /** §3.1 — a `tool` entry: a built-in or custom in-process tool, compared against the tool name. */
668
+ export interface ToolApprovalEntry extends ApprovalEntryCommon {
669
+ type: 'tool';
670
+ matcher: ApprovalMatcher;
671
+ pattern: string | ApprovalHintPattern;
672
+ /** §4.7.4 — optional exact-match bound on the call's host. Tool subjects only. */
673
+ host?: string;
674
+ }
675
+ /** §3.1 — an `mcpTool` entry: one server's tool. */
676
+ export interface McpToolApprovalEntry extends ApprovalEntryCommon {
677
+ type: 'mcpTool';
678
+ matcher: ApprovalMatcher;
679
+ pattern: string | ApprovalHintPattern;
680
+ /**
681
+ * §4.7.5 — **required**, and the user's own key in `mcpServers`: the only identity a server has
682
+ * that is stable, unique and user-authored. The literal `*` means every server, which is why a
683
+ * configured server may not be named `*`.
684
+ */
685
+ server: string;
686
+ /** §4.7.4 — optional exact-match bound on the call's host. */
687
+ host?: string;
191
688
  }
192
689
  /**
193
- * Whether the EXT-10 LLM-as-judge safety gate is enabled for the given dev-tools config.
194
- * Default OFF (only the object form's `judge` truthy enables it), mirroring
195
- * {@link isShellToolEnabled}. A bare `shell: true` keeps the judge OFF — it costs an LLM call
196
- * per command and must be opted into explicitly.
690
+ * EXT-71 §3.1 **one entry** in `allow`, `deny` or `escalate`. All three lists take the same
691
+ * shape; the list an entry sits in decides only what a match DOES (deny over escalate over allow).
692
+ *
693
+ * `type`, `matcher` and `pattern` are always required — no field is inferred and no entry reads
694
+ * two ways. The runtime validator is `approvalEntrySchema` in `config/schema.ts`, which is
695
+ * stricter than TypeScript can be: it rejects unknown fields, an empty or unknown-key `hint`
696
+ * pattern, and a `regexp` that does not compile or is over the length cap.
697
+ */
698
+ export type ApprovalEntry = ShellApprovalEntry | ToolApprovalEntry | McpToolApprovalEntry;
699
+ /**
700
+ * EXT-70 §4.7.1/§9 — the approvals relationship with ONE MCP server, keyed by the user's own
701
+ * `mcpServers` config key (§4.7.5 — the only identity a server has that is stable, unique and
702
+ * user-authored; nothing a server declares about itself ever participates).
703
+ *
704
+ * `trustAnnotations` names the hints that are BELIEVED from that server. It is a list rather than a
705
+ * boolean because trusting `readOnlyHint` while distrusting `openWorldHint` is a coherent position
706
+ * and the common one, and because a single "trusted server" flag throws that distinction away for
707
+ * nothing. **Absent or empty means what the default means: nothing external is believed** — every
708
+ * hint of that server's collapses to the MCP fail-closed default, so its declarations cannot
709
+ * perturb any rule.
197
710
  */
198
- export declare function isShellJudgeEnabled(devTools: GthDevToolsConfig | undefined): boolean;
711
+ export interface McpServerApprovalsConfig {
712
+ /** §4.7.1 — the hints believed from this server. Absent or empty trusts nothing. */
713
+ trustAnnotations?: ToolAnnotationHint[];
714
+ }
715
+ /**
716
+ * EXT-70 §4.7/§9 — the `approvals.mcp` block: the per-server relationship, keyed by the user's own
717
+ * `mcpServers` config key.
718
+ *
719
+ * **`defaults` applies to servers NOT named under `servers`** — §9's own gloss. A server that names
720
+ * itself states its own relationship in full, so `{"jira": {}}` trusts nothing however permissive
721
+ * `defaults` is: trust by omission is exactly the failure §4.7.1 exists to prevent, and the fail-
722
+ * closed direction is the one a silent config edit must fall in.
723
+ *
724
+ * A key here is **not** validated against `mcpServers`. A user may write the policy before adding
725
+ * the server, and coupling the two would make config ORDER matter.
726
+ *
727
+ * This block cannot live inside `mcpServers`, which is modelled permissively because it carries
728
+ * runtime objects.
729
+ */
730
+ export interface McpApprovalsConfig {
731
+ /** The relationship with every server not named under {@link servers}. */
732
+ defaults?: McpServerApprovalsConfig;
733
+ /** Per-server relationships, keyed by the user's own `mcpServers` config key (§4.7.5). */
734
+ servers?: Record<string, McpServerApprovalsConfig>;
735
+ }
199
736
  /**
200
- * Resolve the EXT-10 judge gate settings from a dev-tools config, applying safe defaults
201
- * (auto-approve low, do NOT block high). `enabled` reflects {@link isShellJudgeEnabled}.
737
+ * On-disk `approvals` object form (root or per command). The **scalar form is exactly sugar for
738
+ * `{ mode: <value> }`** (§9.1) the union exists so the extras have a home when they are needed,
739
+ * not so there are two ways to say the same thing.
202
740
  */
203
- export declare function getShellJudgeSettings(devTools: GthDevToolsConfig | undefined): ShellJudgeSettings;
741
+ export interface ApprovalsObjectConfig {
742
+ /** The rung. Absent = {@link DEFAULT_APPROVAL_RUNG}. */
743
+ mode?: ApprovalRung;
744
+ /**
745
+ * §9.1 — the identity profile the rater runs under, as a **bare name** (strict resolution,
746
+ * GS2-62: a name that does not resolve is a hard config error, never a silent fallback).
747
+ * Omitted = the main model. It is the only rater knob; nesting a one-field object is what this
748
+ * design removed.
749
+ */
750
+ rater?: string;
751
+ /** §3 — declared allow-list: what the human has trusted. Read-only input. */
752
+ allow?: ApprovalEntry[];
753
+ /** §3 — declared deny-list: what never runs. Read-only input; applies under `bypass` too. */
754
+ deny?: ApprovalEntry[];
755
+ /**
756
+ * §3/§3.2 — declared escalate list: a match always asks the human, whatever the rung would have
757
+ * done, and with no rating call. Read-only input; inert under `bypass` (§2.5).
758
+ */
759
+ escalate?: ApprovalEntry[];
760
+ /**
761
+ * EXT-66 — wall-clock budget (ms) for ONE rating call. Absent = {@link RATER_DEFAULT_TIMEOUT_MS}
762
+ * (30s), which is a hosted-model number: a local rater is knowably slower, and when it runs out
763
+ * of time the gate escalates, so an unreachable timeout turns the permissive rung into one that
764
+ * asks about everything while every layer reports success.
765
+ */
766
+ raterTimeoutMs?: number;
767
+ /**
768
+ * EXT-70 §4.7/§9 — the per-server MCP relationship. Read through
769
+ * `createEffectiveToolAnnotationSource` (`core/approvals/annotations.ts`), which is the ONE place
770
+ * an effective annotation set is derived; nothing else re-reads this block.
771
+ */
772
+ mcp?: McpApprovalsConfig;
773
+ }
774
+ /** On-disk `approvals` value: the rung on its own, or the object when the extras are needed. */
775
+ export type ApprovalsConfig = ApprovalRung | ApprovalsObjectConfig;
204
776
  /**
205
- * Resolve the {@link GthDevToolsConfig} that applies to the active command, mirroring the
206
- * per-command selection in `builtInToolsConfig.getDefaultTools` (which is what actually emits
207
- * the dev tools) and `GthDeepAgent.getEffectiveDevToolsConfig`: `exec` `commands.exec`,
208
- * `ask --write` → `commands.ask`, `code` → `commands.code`; `undefined` elsewhere (the
209
- * toolkit is inert there). Shared in core so the runner's allow-list gate stays in lockstep
210
- * with where the shell tool is actually emitted.
777
+ * The fully-defaulted approvals posture for one command. `allow`/`deny`/`escalate` are the
778
+ * **declared** lists straight from config — read-only input that the runner merges with the
779
+ * runtime stores the escalation menu writes, and that is never written back to config (§9.1).
780
+ */
781
+ export interface ResolvedApprovals {
782
+ /** The rung in force. */
783
+ rung: ApprovalRung;
784
+ /** Identity profile the rater runs under, or `undefined` for the session model. */
785
+ rater?: string;
786
+ /** Declared allow-list entries (§3.1). Empty when none are declared. */
787
+ allow: ApprovalEntry[];
788
+ /** Declared deny-list entries (§3.1). Empty when none are declared. */
789
+ deny: ApprovalEntry[];
790
+ /** Declared escalate-list entries (§3.1). Empty when none are declared. */
791
+ escalate: ApprovalEntry[];
792
+ /**
793
+ * EXT-66 — wall-clock budget (ms) for one rating call, or `undefined` to let the rater apply
794
+ * `RATER_DEFAULT_TIMEOUT_MS`. Left `undefined` rather than defaulted here so the effective-config
795
+ * snapshot does not churn, exactly as `rater` is.
796
+ */
797
+ raterTimeoutMs?: number;
798
+ /**
799
+ * EXT-70 §4.7 — the declared per-server MCP relationship, or `undefined` when no scope states
800
+ * one (which reads the same as an empty block: nothing external is believed). Left `undefined`
801
+ * rather than defaulted here for the same reason `rater` is — so the effective-config snapshot
802
+ * does not churn.
803
+ */
804
+ mcp?: McpApprovalsConfig;
805
+ }
806
+ /**
807
+ * CFG-26 — how many command prefixes the allow-list holds, for the `/approvals` display.
808
+ * `always: undefined` means the persisted store has not been loaded — rendered `—` rather than a
809
+ * misleading `0`, since a display must never create the store.
810
+ */
811
+ export interface AllowlistCounts {
812
+ session: number;
813
+ always: number | undefined;
814
+ }
815
+ /**
816
+ * EXT-70 §4.7.1 — what one server's annotations are believed on, for display.
817
+ *
818
+ * `configured` says whether {@link server} is a key under `mcpServers` in the loaded config. It is
819
+ * an advisory rather than a validity test: §9 deliberately does NOT check `approvals.mcp` keys
820
+ * against `mcpServers`, so a user may write the policy before adding the server and config ORDER
821
+ * never matters. What it buys interactively is that a mistyped key — which trusts nothing while
822
+ * reading as though it did — can be pointed out rather than swallowed.
823
+ */
824
+ export interface McpServerAnnotationTrust {
825
+ /** §4.7.5 — the user's own `mcpServers` config key. */
826
+ server: string;
827
+ /** The hints believed from this server, resolved through `defaults` where it is not named. */
828
+ trusted: ToolAnnotationHint[];
829
+ /** Whether this key names a server in the loaded config's `mcpServers`. */
830
+ configured: boolean;
831
+ }
832
+ /** EXT-70 §4.7.1 — the whole believed-annotation picture, for the `/approvals` display. */
833
+ export interface McpAnnotationTrustView {
834
+ /** §9 — the hints believed from servers NOT named under `servers`. */
835
+ defaults: ToolAnnotationHint[];
836
+ /** Per-server, for every key either the config names or the policy does. */
837
+ servers: McpServerAnnotationTrust[];
838
+ }
839
+ /**
840
+ * EXT-70 §4.7.1/§4.7.4 — the outcome of believing (or ceasing to believe) hints from one server,
841
+ * so the surface that asked can report exactly what landed and what it costs.
842
+ *
843
+ * **`weakening` is the field that keeps the human un-surprised.** Withdrawing trust pushes a hint
844
+ * back to its fail-closed default, which for three of the four is a *weakening*, so the saved
845
+ * approvals made for that server while the hint was believed will be invalidated (§4.7.4) at the
846
+ * next call to that tool. That has to be said where the user withdraws trust, not only in the
847
+ * notice that arrives later.
848
+ */
849
+ export interface McpAnnotationTrustChange extends McpServerAnnotationTrust {
850
+ /** Hints this change started believing (absent from the previous set). */
851
+ added: ToolAnnotationHint[];
852
+ /** Hints this change stopped believing (present in the previous set). */
853
+ removed: ToolAnnotationHint[];
854
+ /**
855
+ * §4.7.4 — the subset of {@link removed} whose withdrawal can weaken an effective set, and
856
+ * therefore invalidate a grant. Empty on a grant of trust, which can never weaken: every
857
+ * weakening move ends at the fail-closed default, and believing a hint only ever moves away
858
+ * from it.
859
+ */
860
+ weakening: ToolAnnotationHint[];
861
+ /**
862
+ * §4.7.4 — the saved approvals for this server that the *resulting* trust actually weakens, each
863
+ * rendered by `describeApprovalEntry`. They are the ones that will be withdrawn, with the §4.7.4
864
+ * notice, at the next call to that tool.
865
+ *
866
+ * **A prediction, never a deletion.** Invalidation stays scoped to the call being decided, because
867
+ * a sweep would read every held grant against a source that can only answer for the tools
868
+ * registered right now — a server that happened to be offline would read as having weakened
869
+ * everything it ever declared, and the grants would go. Listing them is safe where deleting them
870
+ * is not.
871
+ *
872
+ * **It over-reports in two distinct ways, and both are the same trade.** The comparison is each
873
+ * grant's recorded snapshot against the set in force *now*, not the set before this change against
874
+ * the set after it. So (1) a server that is offline when trust moves declares nothing, resolves to
875
+ * the fail-closed constant, and every grant it holds is named; and (2) a grant already weakened
876
+ * for some other reason — an earlier withdrawal, a `tools/list` that took a hint back — is named
877
+ * under whichever withdrawal happens to run next, including one that moved nothing relevant to it.
878
+ * A named grant is therefore one that the trust now in force weakens, which is what the user needs
879
+ * to know; it is not a claim that *this* withdrawal is what weakened it. Reading it as the latter
880
+ * is how a test comes to assert causation the field never promised.
881
+ *
882
+ * Empty is likewise not "nothing is at risk" but "nothing this session can see is": with
883
+ * {@link weakening} non-empty the rule still holds for any grant made while those hints were
884
+ * believed, which is what the notice says in that case. Counted read-only, like
885
+ * {@link AllowlistCounts}: the persisted store is consulted only when it is already loaded.
886
+ */
887
+ invalidates: string[];
888
+ }
889
+ /**
890
+ * §1.1 — **the default rung is `assisted`, everywhere.** It is the default in every interactive
891
+ * context, it does NOT vary with the configured model, and there is no separate non-interactive
892
+ * default. What changes without a human is what an escalation *does* (§6.2: an immediate non-zero
893
+ * exit, never an approval), not which rung the session starts on. A context-dependent default
894
+ * would reintroduce exactly the hidden branching this ladder exists to remove.
895
+ */
896
+ export declare const DEFAULT_APPROVAL_RUNG: ApprovalRung;
897
+ /**
898
+ * CFG-27 — resolve the effective {@link ResolvedApprovals} for the active command.
899
+ *
900
+ * There is no defaults *matrix*: §1.1 makes `assisted` the default in every context, so this
901
+ * resolver neither detects nor accepts a "context". Precedence is the only thing it decides, and
902
+ * §9.1 splits it in two:
903
+ *
904
+ * - **The scalars — `mode`, `rater`, `raterTimeoutMs` — are replaced** when the per-command value
905
+ * states them and **inherited from the root when it does not**. So the scalar sugar
906
+ * `"code": { "approvals": "bypass" }` is exactly `{ mode: 'bypass' }` merged over the root: it
907
+ * sets the rung and nothing else.
908
+ * - **`deny` and `escalate` never replace: they CONCATENATE across every scope.** A
909
+ * command-specific `deny` *adds to* the root's. Removing an inherited prohibition for one command
910
+ * is deliberately not expressible.
911
+ * - **`allow` is REPLACED when the per-command value states its own, and inherited when it does
912
+ * not.** A per-command scope may therefore narrow what runs unprompted, and may never widen what
913
+ * is prohibited.
914
+ * - **`mcp` (EXT-70 §4.7) follows `allow`, not the restrictive lists**: replaced when the
915
+ * per-command value states it, inherited when it does not. Believing a hint is a PERMISSIVE act
916
+ * in both directions — it can make an `allow` hint entry fire and can make a `deny` hint entry
917
+ * stop firing — so it merges the way the permissive list does, and a per-command scope can
918
+ * narrow the session's trust (`"mcp": {}` believes nothing) but never inherits half of it by
919
+ * accident. Deep-merging the two scopes' `servers` maps was rejected for the same reason: it
920
+ * would leave a deliberately distrustful per-command block silently carrying the root's trust.
921
+ *
922
+ * **The two halves differ because the costs differ (§3.1), not for tidiness.** A missed allow entry
923
+ * escalates and a missed deny entry falls through to the rater — neither is an execution — while a
924
+ * too-broad allow entry *runs, unrated and unprompted*. Concatenating the restrictive lists fails
925
+ * toward a prompt; concatenating the permissive one fails toward an execution, and would leave a
926
+ * deliberately restrictive per-command rung with no way to shed the root's standing grants. Do not
927
+ * "regularize" these three into one policy: the direction each list fails in is the whole design.
928
+ *
929
+ * On the restrictive side the pressure runs the other way (§11.1f). Were the per-command value to
930
+ * replace the root wholesale, the friendliest spelling of "stop asking me about `code`" would also
931
+ * delete every `deny` entry — at the one rung where the deny list and the §8 floor are the only
932
+ * checks left. A prohibition a nested config key can quietly delete is not a hardline.
933
+ *
934
+ * Concatenation order cannot change any outcome (`resolveApprovalRules` consults every deny entry
935
+ * before any escalate entry and every escalate entry before any allow entry), so root-first is a
936
+ * convention for readability — matching `GthAgentRunner.approvalRuleLists`, where the declared
937
+ * entries precede the runtime grants — and never a precedence.
938
+ *
939
+ * Defaults are applied HERE, at the read site, rather than in `DEFAULT_CONFIG` — so the
940
+ * effective-config snapshot the `/config` panel renders never churns (à la GS2-34
941
+ * `injectModelContext` / GS2-63 `output.header`).
942
+ *
943
+ * This is the per-command half. The cross-LAYER half (a project config's lists adding to a global
944
+ * config's rather than replacing them) is the additive-array policy in `config/loader.ts`; both are
945
+ * needed, since either alone still loses a list silently.
946
+ *
947
+ * @param command The active command; selects the per-command block.
948
+ */
949
+ export declare function resolveApprovals(config: Pick<GthConfig, 'commands' | 'approvals'> | undefined, command: GthCommand | undefined): ResolvedApprovals;
950
+ /** A status notice a backend should surface after resolving the shell approval gate. */
951
+ export interface ShellApprovalGateNotice {
952
+ /** Severity to pass to the agent's `statusUpdate` callback. */
953
+ level: StatusLevel;
954
+ /** The user-facing message. */
955
+ message: string;
956
+ }
957
+ /** The resolved shell approval-gate policy: whether to gate, and what to tell the user. */
958
+ export interface ShellApprovalGateDecision {
959
+ /**
960
+ * Whether `run_shell_command` must be wired behind the per-command approval interrupt
961
+ * (langchain's `humanInTheLoopMiddleware`).
962
+ */
963
+ gateShell: boolean;
964
+ /** The notice to surface, when this configuration warrants one. */
965
+ notice?: ShellApprovalGateNotice;
966
+ }
967
+ /**
968
+ * EXT-52 — the ONE shell approval-gate policy every agent backend resolves
969
+ * (`GthLangChainAgent` = lean/default). It decides whether the opt-in `run_shell_command` tool is
970
+ * gated behind the per-command approval interrupt, and which status notice (if any) the backend
971
+ * should surface. A backend contributes only HOW it installs the interrupt; the policy and its
972
+ * user-facing copy live here so no two can drift (and so a later rename of this config surface has
973
+ * one place to change).
974
+ *
975
+ * CFG-27 — **the tool is gated whenever it is enabled, at every rung including `bypass`.** CFG-26
976
+ * used to leave it UNGATED under `bypass` outside interactive `code`, which the ladder cannot
977
+ * afford: §2.5 makes the declared **deny list the one check `bypass` keeps**, and a deny entry can
978
+ * only fire if the call reaches `GthAgentRunner.decideToolApproval` — an ungated call never does.
979
+ * Gating unconditionally also keeps the rung switchable mid-session (`/approvals <rung>`), since a
980
+ * tool wired without the interrupt cannot be re-gated without rebuilding the agent.
981
+ *
982
+ * What each rung then does is decided in `decideToolApproval`, not here:
983
+ * • `bypass` — deny list, then approve without prompting or rating.
984
+ * • `manual`/`write` — deny list, allow-list, else escalate to the human.
985
+ * • `assisted`/`auto` — deny list, allow-list, then the rater.
986
+ *
987
+ * **This decides the SHELL's gating only, and it is not the whole gated set.** With the shell tool
988
+ * disabled — or on a non-dev-tools command (chat/api/…) — nothing about the shell is gated and
989
+ * nothing is announced, but at `manual` and `write` {@link resolveGatedToolNames} still gates
990
+ * every bound tool the rung does not auto-grant, so an MCP call in a plain `chat` session is
991
+ * escalated there. Read `gateShell` as "does the shell need the interrupt", never as "is the
992
+ * interrupt needed at all".
993
+ *
994
+ * Shell enablement itself is resolved through {@link getEffectiveDevToolsConfig} +
995
+ * {@link isShellToolEnabled}, so the gate stays in lockstep with where `GthDevToolkit` actually
996
+ * emits the tool; the posture comes from {@link resolveApprovals}, so this and the runner can
997
+ * never disagree about which rung is in force.
211
998
  */
212
- export declare function getEffectiveDevToolsConfig(config: Pick<GthConfig, 'commands' | 'askWriteMode'> | undefined, command: GthCommand | undefined): GthDevToolsConfig | undefined;
999
+ export declare function resolveShellApprovalGate(config: Pick<GthConfig, 'commands' | 'builtInTools' | 'askWriteMode' | 'approvals'> | undefined, command: GthCommand | undefined): ShellApprovalGateDecision;
1000
+ export {};