@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,6 +1,41 @@
1
- import { GthConfig } from '#src/config.js';
1
+ import { type AllowlistCounts, type ApprovalRung, GthConfig, type McpAnnotationTrustChange, type McpAnnotationTrustView, type ResolvedApprovals, type ToolAnnotationHint } from '#src/config.js';
2
2
  import { BaseCheckpointSaver } from '@langchain/langgraph';
3
- import { AgentResolvers, AgentStreamEvent, GthAgentFactory, GthAgentInterface, GthCommand, GthRunStats, Message, StatusUpdateCallback, ToolApprovalCallback } from '#src/core/types.js';
3
+ import { AgentResolvers, AgentStreamEvent, type AttackHaltCallback, GthAgentFactory, GthAgentInterface, GthCommand, GthRunStats, Message, StatusUpdateCallback, ToolApprovalCallback } from '#src/core/types.js';
4
+ import { type ApprovalGrant } from '#src/core/approvals/grants.js';
5
+ import { type ApprovalDecisionCapture } from '#src/core/shell/approvalCapture.js';
6
+ /**
7
+ * EXT-114 — the page describing the `subagents` config key, pointed at rather than paraphrased in
8
+ * the notice itself.
9
+ *
10
+ * **A GitHub blob URL, matching the other user-facing runtime doc links in this repo** (the
11
+ * approvals-protection pointer in `config/shell-policy.ts` and the 2.0 migration pointer in
12
+ * `config/schema.ts`) — a running CLI's user has no checkout for a relative path to resolve in.
13
+ * `subagentScope.spec.ts` pins that this URL's anchor still resolves to a real heading in the page
14
+ * it names, because a doc link that silently rots is worse than no link.
15
+ */
16
+ export declare const SUBAGENTS_DOCS_URL = "https://github.com/pukeko-robotics/gaunt-sloth/blob/main/docs/configuration/profiles.md#named-profile-subagents-subagents";
17
+ /** Options for {@link GthAgentRunner#init} that qualify the run without changing how it behaves. */
18
+ export interface GthAgentRunnerInitOptions {
19
+ /**
20
+ * GS2-81 — the CLI verb this run belongs to, for messages only, when `command` is deliberately
21
+ * `undefined`. It is a SEPARATE input from `command` because `command` is not a label: it selects
22
+ * the mode prompt (`readModePrompt`), the per-command approvals posture (`resolveApprovals`) and
23
+ * the command-specific filesystem config, so a helper agent that must run on the chat prompt —
24
+ * the `gth pr` change-requirements discovery agent — cannot borrow it to say which verb it serves.
25
+ * Nothing but the wording of a notice reads this.
26
+ */
27
+ owningCommand?: GthCommand;
28
+ /**
29
+ * GS2-95 — the name of the command the USER typed, for the run header only (`eval`, `batch`,
30
+ * `workflow`, `gth-batch`). Forwarded verbatim to the agent's own `displayCommand` init option;
31
+ * the runner reads nothing from it.
32
+ *
33
+ * Separate from `command` for the same reason `owningCommand` is, and a distinct field from it:
34
+ * `owningCommand` says which verb a command-less helper agent serves, this says what to CALL a
35
+ * run that does have a verb but does not go by its name.
36
+ */
37
+ displayCommand?: string;
38
+ }
4
39
  /**
5
40
  * Agent simplifies interaction with LLM and reduces it to calling a few methods
6
41
  * {@link GthAgentRunner#init} and {@link GthAgentRunner#processMessages}.
@@ -19,6 +54,14 @@ export declare class GthAgentRunner {
19
54
  * for non-interactive entrypoints (a scripted `exec` run with no TTY to prompt on).
20
55
  */
21
56
  private toolApprovalCallback;
57
+ /**
58
+ * [[TUI-C68]] §6.1 — consumer hook invoked when the rater rates a command an `attack`, so an
59
+ * interactive surface can show the red banner before the run ends. Set via
60
+ * {@link setAttackHaltCallback}; **when unset the runner halts immediately**, which is the
61
+ * behaviour every surface had before a banner existed. A surface that forgets to wire it
62
+ * therefore keeps the halt rather than losing it.
63
+ */
64
+ private attackHaltCallback;
22
65
  /** The command the runner was initialized for; selects which `devTools` config applies. */
23
66
  private command;
24
67
  /**
@@ -29,33 +72,68 @@ export declare class GthAgentRunner {
29
72
  */
30
73
  private lastRunStats;
31
74
  /**
32
- * EXT-12 — runtime, session-scoped yolo flag toggled by the `/yolo` slash command. Distinct
33
- * from the static `devTools.shellYolo` config flag (which omits the tool from `interruptOn` at
34
- * agent-build time and cannot be changed mid-session). Because the tool stays gated (in
35
- * `interruptOn`), this flag is consulted at the TOP of {@link decideToolApproval}: when ON, a
36
- * gated `run_shell_command` is auto-approved WITHOUT prompting (yolo behaviour) for the rest of
37
- * this runner's life. Never persisted; defaults OFF. It does NOT disable the hardline floor —
38
- * catastrophic commands are still refused at exec time in `GthDevToolkit.executeCommand`.
75
+ * CFG-27the runtime, session-scoped approvals posture, seeded at {@link init} from
76
+ * {@link resolveApprovals} and thereafter switchable for the session by `/approvals <rung>`.
77
+ * **This field, not the interrupt wiring, is where the rung lives.** The backends wire the
78
+ * interrupt rung-independently, so every tool any rung could gate arrives at the top of
79
+ * {@link decideToolApproval} and is judged against the rung recorded here which is what makes
80
+ * `/approvals manual` take effect mid-session, and what keeps a config that pre-selects
81
+ * `bypass` switchable back. Never persisted.
82
+ *
83
+ * It does NOT disable the hardline floor — catastrophic commands are still refused at exec time
84
+ * in `GthDevToolkit.executeCommand` under every rung.
85
+ */
86
+ private sessionApprovals;
87
+ /**
88
+ * CFG-26 — the model the AI rater rates with, when `approvals.rater.profile` names an identity
89
+ * profile. Resolved ONCE at {@link init} (never mid-turn) and handed to `rateShellCommand`;
90
+ * `undefined` means no profile is configured and the rater uses the session model.
91
+ */
92
+ private raterModel;
93
+ /**
94
+ * EXT-66 — how many rating calls this session gave up on. Counted so the notice can say "3 times
95
+ * this session" rather than repeating an identical line, and so a silent drift toward
96
+ * escalate-everything has a number attached to it.
97
+ */
98
+ private raterTimeouts;
99
+ /**
100
+ * EXT-71 §3.1/§6 — what the escalation menu granted at run time, for the life of THIS runner
101
+ * instance: {@link ApprovalEntry} objects, never prefixes, and never anything from config (the
102
+ * declared lists are read-only input consulted straight from the posture). Instance-scoped so
103
+ * concurrent sessions (ACP / AG-UI multi-session) cannot stomp each other's approvals.
104
+ */
105
+ private readonly sessionGrants;
106
+ /**
107
+ * CFG-27 §3 — what the escalation menu's *always reject* choice adds at run time, in the same
108
+ * grammar ([[TUI-C26]] wires that writer; the store speaks it already). The entries DECLARED in
109
+ * `approvals.deny` are not here — they are read-only config input, matched from the posture — and
110
+ * both are handed to the same matcher, so a runtime refusal and a declared one are one list.
111
+ */
112
+ private denyGrants;
113
+ /**
114
+ * The persisted (`always`) grant store, loaded lazily on first use from
115
+ * `.gsloth/.gsloth-settings/shell-allowlist.json`. Null until a gated call actually needs it, and
116
+ * null when the file cannot be loaded at all (in which case `always` grants degrade to session).
39
117
  */
40
- private sessionYolo;
118
+ private persistedGrants;
119
+ private persistedGrantsLoaded;
41
120
  /**
42
- * EXT-9 Tier-2 session allow-list approved command prefixes that auto-approve for the
43
- * life of THIS runner instance. Instance-scoped (not module-global) so concurrent
44
- * sessions (ACP / AG-UI multi-session) cannot stomp each other's approvals.
121
+ * [[EXT-29]] §5 the state of the agent↔rater negotiation at `auto`: the transcript, §5.3's
122
+ * consecutive-rejection counter and the reachability bound. Instance-scoped for the same reason
123
+ * the grant stores are — a concurrent ACP / AG-UI session must not inherit another's argument.
45
124
  */
46
- private readonly sessionAllowlist;
125
+ private readonly negotiation;
47
126
  /**
48
- * EXT-9 Tier-2 persisted (`always`) allow-list, loaded lazily on first use from
49
- * `.gsloth/.gsloth-settings/shell-allowlist.json`. Null until the shell tool is gated
50
- * and the allow-list is enabled; null also when persistence is disabled by config.
127
+ * [[TUI-C27]] the diagnostic record of every gated decision this session made, for
128
+ * `/debug-dump`. Instance-scoped for the same reason the negotiation and the grant stores are: a
129
+ * concurrent ACP / AG-UI session must not inherit another's approvals history, and a dump taken
130
+ * in one must not describe the other.
51
131
  */
52
- private persistedAllowlist;
53
- private persistedAllowlistLoaded;
132
+ private readonly approvalCaptures;
54
133
  /**
55
134
  * @param agentFactory Produces the {@link GthAgentInterface} the runner drives.
56
- * Defaults to the lean {@link GthLangChainAgent} (core). `@gaunt-sloth/agent`
57
- * passes a factory returning a deep `GthDeepAgent` so the same runner can drive a
58
- * `createDeepAgent` graph without core depending on deepagents.
135
+ * Defaults to the lean {@link GthLangChainAgent} (core). The seam stays parameterised so a
136
+ * caller can drive the runner with a different graph builder without core depending on it.
59
137
  */
60
138
  constructor(statusUpdate: StatusUpdateCallback, resolvers?: AgentResolvers, agentFactory?: GthAgentFactory);
61
139
  /**
@@ -65,20 +143,154 @@ export declare class GthAgentRunner {
65
143
  */
66
144
  setToolApprovalCallback(callback: ToolApprovalCallback | null): void;
67
145
  /**
68
- * EXT-12flip the runtime, session-scoped yolo flag (the `/yolo` slash command). When ON,
69
- * gated `run_shell_command` calls auto-approve without prompting for the rest of this session;
70
- * the hardline floor still applies at exec time. Returns the NEW state so the caller can render
71
- * a notice. Session-scoped only nothing is written to config.
146
+ * [[TUI-C68]] §6.1 register the handler that shows the **attack banner**, the one way a human
147
+ * gets past an `attack` verdict. Pass `null` to clear.
148
+ *
149
+ * Separate from {@link setToolApprovalCallback} because it is a separate question with an
150
+ * inverted default: an absent approval callback means *this session has nobody to ask*, and an
151
+ * absent one here means *end the run*. Wiring it is what an interactive surface opts into; every
152
+ * other surface keeps the halt (see {@link attackHaltCallback}).
153
+ */
154
+ setAttackHaltCallback(callback: AttackHaltCallback | null): void;
155
+ /**
156
+ * §6.1 — **the single seam between an `attack` verdict and the end of the run.** Both rating
157
+ * paths — the §3.2 allow-match tripwire and the ordinary rater decision — go through here, so the
158
+ * banner cannot be present on one and missing on the other, which is the shape of bug that leaves
159
+ * a halt answerable in some sessions and not others with nothing on screen to tell them apart.
160
+ *
161
+ * It returns a decision for the one answer that grants and throws for everything else:
162
+ *
163
+ * - **no callback → throw**, immediately and unchanged. §6.2's rule is that a run with nobody to
164
+ * ask never blocks and never times out into a grant; the way that is guaranteed is that waiting
165
+ * is something only a wired surface can cause.
166
+ * - **`run-anyway` → approve, scope `once`.** Exactly one command runs. `once` is not a default
167
+ * restated: it is what keeps §6.1's three "never"s true. Returning here is also returning from
168
+ * *before* the block that records a sticky grant, so no allow-list entry and no session grant
169
+ * can be written on this path — the next identical call is rated again and reaches this banner
170
+ * again. Nothing here touches the rung, and nothing disables the rater, the escalation or the
171
+ * halt for anything else.
172
+ * - **anything else → throw.** `stop`, and equally a value a surface invents or forgets to
173
+ * return: the grant is one exact answer and everything else is a refusal.
174
+ */
175
+ private haltOrRunAnyway;
176
+ /**
177
+ * CFG-27 — switch the session-scoped rung (`/approvals <rung>`). Idempotent; returns the NEW
178
+ * rung so the caller can render a notice. Session-scoped only — nothing is written to config,
179
+ * and the declared allow/deny lists are unaffected (they are config input, not session state).
72
180
  */
73
- toggleSessionYolo(): boolean;
74
- /** EXT-12 — current state of the runtime session-scoped yolo flag (see {@link toggleSessionYolo}). */
75
- isSessionYolo(): boolean;
181
+ setSessionApprovalRung(rung: ApprovalRung): ApprovalRung;
182
+ /** CFG-27the session's current approvals posture (rung + rater profile + declared lists). */
183
+ getSessionApprovals(): ResolvedApprovals;
184
+ /**
185
+ * [[TUI-C27]] — every gated decision this session made, oldest first, for the `/debug-dump`
186
+ * archive.
187
+ *
188
+ * Threaded by each surface into `writeDebugDump`, exactly as `agent.lastModelRequest` is: the
189
+ * writer redacts it with the same pass it applies to every other artifact, and a surface that
190
+ * does not thread it simply omits the file.
191
+ */
192
+ getApprovalCaptures(): ApprovalDecisionCapture[];
193
+ /**
194
+ * CFG-26 — the allow-list sizes for the `/approvals` display: how many command prefixes the
195
+ * human has trusted this session, and how many are persisted in the project file.
196
+ *
197
+ * READ-ONLY BY CONSTRUCTION: it reports the persisted count only when the store has ALREADY
198
+ * been loaded (or persistence is on and it can be read), and never through a path that would
199
+ * CREATE the store as a side effect of showing a display — a status command must not mutate
200
+ * session state. `always: undefined` therefore means "not loaded / persistence off", which the
201
+ * caller renders as `—` rather than a misleading `0`.
202
+ */
203
+ getAllowlistCounts(): AllowlistCounts;
204
+ /**
205
+ * CFG-27 — the session's deny entries for display: the declared `approvals.deny` entries
206
+ * (rendered one line each) followed by whatever the escalation menu's *always reject* added at
207
+ * run time. Both refuse a call, so both are shown.
208
+ */
209
+ getDenylist(): string[];
210
+ /**
211
+ * §3/§4.7.4 — **the grants themselves**, for an approvals view that shows *what* was granted,
212
+ * *when*, and *under which effective annotations*. The counterpart of {@link getAllowlistCounts},
213
+ * which answers only how many.
214
+ *
215
+ * The declared config lists are deliberately NOT here. They are something a human wrote and
216
+ * reviewed, they carry no `grantedAt` and no scope, and `getAllowlistCounts` already counts them
217
+ * alongside these; mixing them in would present a config line as something the session granted.
218
+ *
219
+ * **Read-only in both senses.** It never loads the persisted store — same rule as
220
+ * {@link getAllowlistCounts}: a display must not create the store in order to show it, so a
221
+ * session that has not yet needed the file lists its session grants alone. And every grant is
222
+ * **deep-copied on the way out**, because the stores hand back their live records: the copy on the
223
+ * way in is what makes a snapshot private to its grant, and handing the same object to a renderer
224
+ * would put what the gate matches against one property assignment away from any consumer.
225
+ */
226
+ getGrants(): ApprovalGrant[];
227
+ /**
228
+ * §4.7.1 — **which of each server's annotation hints this session believes**, for display.
229
+ *
230
+ * Every key either side names is listed: a configured `mcpServers` key with no policy of its own
231
+ * (which resolves through `defaults`), and a policy key naming a server the config does not have
232
+ * (which is what a typo looks like). Resolution is {@link trustedAnnotationHints}, the same
233
+ * function the gate derives effective annotations through, so the display cannot claim a
234
+ * relationship the gate does not act on.
235
+ */
236
+ getMcpAnnotationTrust(): McpAnnotationTrustView;
237
+ /**
238
+ * §4.7.1 — **believe, or stop believing, specific hints from one server**, for the life of this
239
+ * session. The runtime half of `approvals.mcp.servers.<key>.trustAnnotations` (§9), so a user can
240
+ * do from the TUI what they can do in config.
241
+ *
242
+ * **Per hint, never per server.** `hints` names the hints this call moves and leaves every other
243
+ * hint of that server's exactly as it was, because believing a server's `readOnlyHint` while
244
+ * disbelieving its `openWorldHint` is a coherent position and the common one. A "trust this
245
+ * server" flag is the design §4.7.1 rejects.
246
+ *
247
+ * **The previous set is what was IN FORCE, resolved through `defaults`.** A server not named
248
+ * under `servers` inherits `defaults`, and naming it makes it state its relationship in full (§9)
249
+ * — so seeding from the empty set would mean that believing one more hint silently withdrew every
250
+ * hint `defaults` had granted, which is a weakening the user did not ask for and would invalidate
251
+ * their grants.
252
+ *
253
+ * **Session-scoped only.** Nothing is written to config: the declared block is read-only input
254
+ * (§9.1), exactly as the rung is.
255
+ *
256
+ * A trusted external annotation still never grants more than the same annotation grants one of
257
+ * our own built-ins — that holds in `core/approvals/annotations.ts` by construction, and this
258
+ * changes only which hints are read.
259
+ */
260
+ setMcpAnnotationTrust(server: string, hints: readonly ToolAnnotationHint[], believe: boolean): McpAnnotationTrustChange;
261
+ /**
262
+ * §4.7.4 — which of this server's saved approvals the trust now in force weakens, for the notice
263
+ * that reports a trust change. **It predicts; it never removes.** The removal stays where Task
264
+ * 4 put it — at the call being decided — because that is the only moment the tool's declaration
265
+ * can be read for certain; here a server that is merely offline declares nothing and would read
266
+ * as having weakened everything.
267
+ *
268
+ * It compares through the same two functions the gate does: the effective-annotation source built
269
+ * from the posture as it stands *after* the change, and `annotationWeakenings`. A second
270
+ * comparison written for the display is how a warning comes to describe a rule the gate does not
271
+ * have.
272
+ */
273
+ private grantsWeakenedByCurrentTrust;
76
274
  /**
77
275
  * Init is split into a separate method. This may create a number of connections,
78
276
  * and we'd better have an instance by that moment, for the case things will go wrong,
79
277
  * so we can wrap init into try-catch and then call {@link #cleanup} within finally.
80
278
  */
81
- init(command: GthCommand | undefined, configIn: GthConfig, checkpointSaver?: BaseCheckpointSaver | undefined): Promise<void>;
279
+ init(command: GthCommand | undefined, configIn: GthConfig, checkpointSaver?: BaseCheckpointSaver | undefined, options?: GthAgentRunnerInitOptions): Promise<void>;
280
+ /**
281
+ * EXT-114 — `subagents` is configurable but not yet dispatched, and a run that declares them must
282
+ * say so rather than start with a quietly smaller toolset than the config describes.
283
+ *
284
+ * The `task` tool that spawned them belonged to the deepagents runtime, which is gone; the lean
285
+ * primitive that replaces it is GS2-25. Until then a declared subagent is inert. Keeping the key
286
+ * valid is deliberate — a config written for GS2-25 should not have to be un-written and
287
+ * re-written — but inert-and-silent is the failure this notice exists to prevent: the parent
288
+ * simply does the work itself, on the parent's model, and the only visible symptom is the bill.
289
+ *
290
+ * Fires on EVERY run rather than only where a backend could once have honoured it, because there
291
+ * is no longer any run that can.
292
+ */
293
+ private warnIfSubagentsCannotBeHonored;
82
294
  /**
83
295
  * processMessages deals with both streaming and non-streaming approaches.
84
296
  */
@@ -98,44 +310,315 @@ export declare class GthAgentRunner {
98
310
  * streamed across all resume turns (empty when nothing was resumed).
99
311
  *
100
312
  * No-ops (returns '') when the agent does not support interrupts (`getPendingToolInterrupts`/
101
- * `streamResume` absent), so the lean agent and non-HITL configs are unaffected.
313
+ * `streamResume` absent) that is the only exemption. As of EXT-52 BOTH backends gate
314
+ * `run_shell_command` and expose the interrupt surface, so the lean (default) agent is now
315
+ * exactly the agent this loop serves; only an agent implementation without those methods
316
+ * (e.g. a test double) skips it.
102
317
  */
103
318
  private resolveToolInterrupts;
104
319
  /**
105
- * Decide a single pending tool call (EXT-9 Tier-2). For the opt-in `run_shell_command`,
106
- * consult the scoped allow-list FIRST: if the command's classified prefix is already
107
- * approved (session or persisted `always`) and survives the safe-bin anti-widening
108
- * re-validation, auto-approve SILENTLY (no human prompt). Otherwise fall through to the
109
- * human callback; when the human grants `session`/`always` scope, record the command's
110
- * classified prefix into the matching store so future flag-variants stop re-prompting.
320
+ * EXT-71 §3.1/§3.2, EXT-70 §4.7.5 — the subject a pending tool call presents to the rule matcher.
321
+ *
322
+ * A gated `run_shell_command` is a **shell** subject and nothing else: it is matched by `shell`
323
+ * entries, against the command. It is deliberately NOT also offered as a `tool` subject named
324
+ * `run_shell_command`, which would create a second allow path to every shell command carrying a
325
+ * different §3.2 `rate` default and a match that never saw the command it was approving.
326
+ *
327
+ * **Everything else splits by provenance**, which is the distinction §4.7.1 rests on: `tool` is
328
+ * the TRUSTED provenance, read verbatim, so an MCP tool arriving as one would be asking the
329
+ * trusted path for a third party's annotations — a gate any server can opt itself out of. Every
330
+ * MCP-namespaced name therefore becomes an `mcpTool` subject carrying the user's own `mcpServers`
331
+ * key, and one whose server cannot be resolved stays an `mcpTool` subject under an unnameable
332
+ * server rather than falling back to `tool` (see `approvalSubjectForToolName`).
333
+ *
334
+ * **The host (§4.7.4)** is attached here, so the one subject the whole decision runs on carries
335
+ * it: the rule matcher treats a `host` on an entry as an additional exact-match condition, and a
336
+ * grant the menu writes records it. A call naming no single host has none, which fails toward a
337
+ * prompt at both sites.
338
+ *
339
+ * Widening which tools the gate actually suspends on is still [[EXT-30]]; this decides what a
340
+ * suspended call *is* whenever one arrives.
341
+ *
342
+ * @param hosts Every distinct host the call's arguments name ({@link toolCallHosts}).
343
+ */
344
+ private approvalSubjectFor;
345
+ /**
346
+ * §4.7.5 — the user's own `mcpServers` keys, the only identity a server has here. Own enumerable
347
+ * keys via `Object.keys`, so nothing inherited can pose as a configured server.
348
+ */
349
+ private configuredMcpServerKeys;
350
+ /**
351
+ * EXT-70 §4.7.1 — the source a `hint` entry reads a call's EFFECTIVE annotations through, built
352
+ * from the session's `approvals.mcp` block and the two declared-annotation lookups.
353
+ *
354
+ * Built per decision rather than cached at {@link init}, for two reasons that both bite: the
355
+ * agent registers its tools *inside* `agent.init()`, so an init-time snapshot would be empty; and
356
+ * a re-init re-resolves the tool list, which for MCP may hand back different declarations.
357
+ *
358
+ * The two lookups are deliberately different in kind. `builtIn` reads OUR OWN authored table and
359
+ * never the bound tool list — the bound list contains every server's tools, and a `builtIn`
360
+ * lookup over it would read a third party's declaration through the trusted-verbatim path.
361
+ * `mcp` reads what the servers declared, keyed by the registered tool name so the server key is
362
+ * never split apart and re-joined differently.
363
+ */
364
+ private effectiveToolAnnotationSource;
365
+ /**
366
+ * Decide a single pending tool call. Spec order — **deny → bypass → escalate → allow → rater →
367
+ * human prompt**, with the hardline floor at exec time regardless. The two adjacencies that carry
368
+ * the design are that deny comes BEFORE `bypass` and escalate comes AFTER it:
111
369
  *
112
- * When no human callback is wired (non-TTY exec run) and nothing is allow-listed, reject
113
- * never auto-approve. Non-shell tools (or any tool when the allow-list is disabled) skip the
114
- * allow-list and go straight to the human callback / default-reject, preserving prior behaviour.
370
+ * 1. **deny** (§3) a declared entry or a runtime *always reject* grant is refused with no
371
+ * prompt and no rating call. It is consulted FIRST, and it is the one
372
+ * check that **still applies under `bypass`**: choosing `bypass` says *"stop asking me"*, not
373
+ * *"forget what I told you never to do"*. A deny entry MAY match a compound command, because a
374
+ * prohibition that catches something unresolvable errs in the direction that costs nothing.
375
+ * 2. **`bypass`** — the gate is off for this session; approve at scope `once`.
376
+ * 3. **escalate** (§3.2) — a declared entry always asks the human, whatever the rung would have
377
+ * done, **including outranking the automatic grants of `manual` and `write`** and any allow
378
+ * entry that also matched. It goes straight to the human with **no rating call**, and it never
379
+ * enters the `auto` negotiation. It is **inert at `bypass`**, which is why it sits below
380
+ * the rung check: the rung chosen for this session wins, and a stop that must survive `bypass`
381
+ * is a deny entry and only that.
382
+ * 4. **allow** (§3, §3.2) — a declared entry or a grant the human made at an earlier prompt this
383
+ * session (or persisted), matched against the whole normalized command and only when that
384
+ * command statically resolves. An allow match settles the human's part: no prompt. Whether the rater
385
+ * still reviews the call is the entry's own `rate` (§3.2) — honored at the rater rungs and
386
+ * inert at the deterministic ones, so no entry can smuggle a model call into `manual` or
387
+ * `write` — and a rated allow match is a TRIPWIRE, not a re-adjudication
388
+ * ({@link mapAllowMatchedVerdictToAction}).
389
+ * 5. **auto-rater** (`assisted` / `auto` only) — `safe` approves, `destructive` and
390
+ * `catastrophic` escalate, and `attack` HALTS the run ({@link AttackHaltError}). The other
391
+ * three rungs consult no model at all. A command whose target the gate cannot statically
392
+ * resolve is rated **exactly like any other** ([[EXT-81]]), with a neutral note in the rating
393
+ * prompt naming the shape the parser saw. It used to skip the call and be refused straight
394
+ * back to the model instead; §6.1's rule is that a deterministic layer fires only where it is
395
+ * confident something is a threat, and a parser reporting it could not read a string has
396
+ * detected nothing. At those same two rungs a **tool**
397
+ * call is instead floored deterministically by §4.7.3's open-world rule
398
+ * ({@link openWorldToolFloorReason} into {@link applyDestructiveFloor} — the one floor the
399
+ * shell path also reaches): a call whose effective `openWorldHint` is true is `destructive`,
400
+ * whatever its `readOnlyHint` says.
401
+ * 6. **human prompt** — the approval callback; when the human grants `session`/`always` scope,
402
+ * **that command** is recorded as an `exact` entry (§3.1/§6 — the menu never widens), so the
403
+ * same command stops re-prompting and a longer variant of it still asks.
404
+ *
405
+ * §6.2 — where no human can answer (CI, a one-shot run, a server), an escalation is **not** a
406
+ * rejection handed back to the model: it is an immediate non-zero exit
407
+ * ({@link NonInteractiveEscalationError}) carrying the command, the rating and its reason. No
408
+ * prompt, no waiting, and never a timeout into approval. Declaring commands in `approvals.allow`
409
+ * is the supported way to make a pipeline pass.
115
410
  *
116
411
  * Hardline catastrophic commands remain refused at exec time regardless of any approval here
117
412
  * (defense in depth in `GthDevToolkit.executeCommand`), so an allow-listed `rm -rf /` still
118
413
  * cannot run.
414
+ *
415
+ * **Step 0 is the rung.** The backends wire the interrupt over every tool ANY rung could gate,
416
+ * because the graph is built once and `/approvals <rung>` moves the rung under it for the rest of
417
+ * the session. So a call arriving here has not yet been judged against the rung in force: this is
418
+ * where that happens, on `sessionApprovals.rung`, which a mid-session switch has already updated.
419
+ * A call the live rung does not gate is approved on the spot — no rule matching, no rating, no
420
+ * prompt — which is what keeps `assisted`, `auto` and `bypass` behaving exactly as they did
421
+ * when the interrupt held the shell alone. It sits ABOVE the deny check for the same reason: an
422
+ * ungated call never reached this method at all before, so a deny entry could not fire on one, and
423
+ * a security fix for two rungs is not the place to change that. (The shell is gated at every rung
424
+ * whenever the shell gate is on, so §2.5's rule that the deny list survives `bypass` is untouched.)
119
425
  */
120
426
  private decideToolApproval;
121
- /** Whether the EXT-10 LLM-as-judge safety gate is enabled for the active command's config. */
122
- private isShellJudgeOn;
123
- /** Whether the EXT-9 Tier-2 allow-list is enabled for the active command's devTools config. */
124
- private isShellAllowlistOn;
125
427
  /**
126
- * Lazily load (once per instance) the persisted `always` allow-list, unless persistence is
127
- * disabled by config. Returns null when persistence is off so `always` grants behave as
128
- * `session` (in-memory only).
428
+ * [[TUI-C27]] {@link decideToolApprovalInner} with the record closed off on EVERY exit.
429
+ *
430
+ * The final action is written here rather than at each of the decision's many returns, because
431
+ * "what became of the call" is one fact with one source: what this method returns or throws. A
432
+ * per-return assignment is a list that a new branch joins without noticing, and the branch that
433
+ * would be forgotten is the one that ends the run.
434
+ */
435
+ private recordedDecision;
436
+ /** The decision itself; {@link decideToolApproval} wraps it with §5.3's reset. */
437
+ private decideToolApprovalInner;
438
+ /**
439
+ * [[TUI-C27]] — attribute the deciding stage and hand the decision straight back.
440
+ *
441
+ * A one-liner so a stage can be recorded ON the `return` that carries it rather than on the line
442
+ * above: two statements let an early return be added between them, and the record would then name
443
+ * a stage that did not decide.
444
+ */
445
+ private stage;
446
+ /**
447
+ * One rating call, with EXT-66's timeout reporting attached. Extracted so the §3.2 tripwire (a
448
+ * rated allow match) and the ordinary rater path cannot drift apart in WHAT they hand the rater —
449
+ * only in what they do with the answer.
129
450
  */
130
- private getPersistedAllowlist;
131
- /** Check the command against the session + persisted stores (with anti-widening re-validation). */
132
- private isApprovedByAllowlist;
451
+ private rateCommand;
133
452
  /**
134
- * Record a human-granted approval at the given scope. `once` persists nothing. `session`
135
- * adds the classified prefix to the in-memory store. `always` additionally persists it (or
136
- * falls back to session-only when persistence is disabled).
453
+ * EXT-58 (§4.3/§4.4) — the built-in tools already granted at the session's CURRENT rung, as
454
+ * names plus one-line locally-authored descriptions, for the rater prompt.
455
+ *
456
+ * Two filters make this safe to place outside the rater's fenced untrusted block:
457
+ * - the names come from what the agent actually registered
458
+ * ({@link GthAgentInterface.getRegisteredToolNames}), so the rater can only ever offer a tool
459
+ * this session has;
460
+ * - the descriptions come from core's own `BUILT_IN_TOOL_SUMMARIES` table, so no MCP, custom or
461
+ * A2A tool's own (attacker-influenceable) description can reach the prompt.
462
+ *
463
+ * Empty when the agent does not expose its tools — the rater then gets no list and, per the
464
+ * prompt, offers nothing.
465
+ */
466
+ private getGrantedBuiltInTools;
467
+ /**
468
+ * §3/§3.3 — the three rule lists this session decides by: the DECLARED entries from config
469
+ * (read-only input) concatenated with the runtime grants the escalation menu made. One set of
470
+ * lists, handed to the one comparison engine; the concatenation cannot change any outcome
471
+ * because `resolveApprovalRules` consults every deny entry before any escalate entry and every
472
+ * escalate entry before any allow entry.
473
+ *
474
+ * The persisted store is loaded here rather than at {@link init} — lazily, once per instance, and
475
+ * NEVER at `bypass`, where the allow list is moot and a session that has switched the gate off
476
+ * should not be reading or rewriting the project's grant file.
477
+ */
478
+ private approvalRuleLists;
479
+ /**
480
+ * Lazily load (once per instance) the persisted `always` grant store.
481
+ *
482
+ * CFG-27 removed the `persistAllowlist` switch: §3 makes persistence a per-decision choice in
483
+ * the escalation menu (`approve` forgets, `always approve` persists), and a global "never
484
+ * persist" setting would only duplicate a keystroke. Returns null when the store cannot be
485
+ * loaded at all, in which case `always` grants degrade to `session` (in-memory only).
486
+ *
487
+ * The v1→v2 migration notice is routed to `statusUpdate` from here, which is the only place that
488
+ * knows how to reach the user.
489
+ */
490
+ private getPersistedGrants;
491
+ /**
492
+ * §3.1/§4.7.4/§6 — **the grant a sticky choice would write for this call**, or `undefined` when
493
+ * none is on offer. The one place that question is answered, so the menu's *this is what will be
494
+ * stored* line (§6) and the store can never disagree.
495
+ *
496
+ * - **A shell call** records the command itself as an `exact` entry (§3.1) — never a prefix,
497
+ * never a pattern. One that does not statically resolve (composition, substitution,
498
+ * redirection) is not on offer: no allow entry of any matcher matches such a command, so the
499
+ * entry would be inert, and an inert entry sitting in a list §3 requires to be inspectable
500
+ * tells the user something is in force when nothing is.
501
+ * - **A tool call** records identity — the tool, its server, and the host where the call carries
502
+ * one (§4.7.4, {@link toolGrantEntry}) — never arguments, which would produce a grant that
503
+ * never matches twice. A call naming no host records the tool alone, which is §6's own example
504
+ * (*always approve `mcp__jira__create_issue`*, where no host is involved); what keeps that from
505
+ * being unbounded is §3.2's default that a tool entry is still `rate: true`, so the rater goes
506
+ * on seeing every call's full arguments.
507
+ *
508
+ * Four cases have **no grant on offer at all**, each fail-closed:
509
+ *
510
+ * 1. **`bypass`** — the gate is off for this session and nothing is remembered from it.
511
+ * 2. **`run_shell_command` arriving as a tool subject.** That is what a shell call with no
512
+ * readable `command` argument presents as, and it names no host, so without this it would take
513
+ * the tool-only arm and write a `{"type":"tool","pattern":"run_shell_command"}` grant that
514
+ * auto-approves every future call whose command cannot even be read. This exclusion is what
515
+ * stops that, not a side effect of anything else, and it must survive [[EXT-30]] widening the
516
+ * gate.
517
+ * 3. **A call naming more than one distinct host.** The grammar has no entry for it. `host` is a
518
+ * single optional string on every tool arm of `approvalEntrySchema`, and every arm is a
519
+ * `z.strictObject`, so recording the *set* is not a policy this code may choose — a `hosts`
520
+ * array is an unrecognized-key error, and writing one would be a §3.1 grammar change. Of the
521
+ * two entries that would parse, the host-bound one displays a bound the grant does not have,
522
+ * which §6 forbids (the menu shows exactly what will be stored). And a grammar that did record
523
+ * the set, matching only when all of it recurred, would fail §4.7.4's opening test anyway: a
524
+ * tool whose host set varies per call would get a grant that never matches a second time — not
525
+ * a narrower grant, the useless one §4.7.4 rejects by name.
526
+ *
527
+ * **What this arm does not claim.** It is not a narrowing. A hostless entry imposes no host
528
+ * condition at all (`resolveApprovalRules`), so the tool-only grant that any host-less call to
529
+ * the same tool produces already auto-approves a multi-host one. Refusing here withholds a
530
+ * grant; it does not close a hole, and the reason to keep it is the grammar above rather than
531
+ * any breadth it prevents. Asserted, so this cannot drift back into a claim the system does not
532
+ * support.
533
+ * 4. **An MCP call whose server could not be resolved** ({@link toolGrantEntry} returns `null`) —
534
+ * a call nobody can attribute is not one anything can remember.
535
+ */
536
+ private stickyGrantFor;
537
+ /**
538
+ * [[TUI-C26]] §6 — **the entry the escalation menu's *always reject* choice would record**, or
539
+ * `undefined` when the grammar cannot hold one. The deny mirror of {@link stickyGrantFor}, and a
540
+ * separate function rather than a flag on it, because the two answer different questions.
541
+ *
542
+ * **Nearly every reason an allow entry is withheld does not apply here.** §3 has one rule for
543
+ * this and it runs the other way — *undecidable is a non-match on the allow side and a match on
544
+ * the deny side* — so:
545
+ *
546
+ * - **A command that does not statically resolve gets an entry.** `stickyGrantFor` refuses one
547
+ * because no allow entry of any matcher would ever match it, making the entry inert; a deny
548
+ * entry for the same command is matched against the whole normalized command *and* every
549
+ * segment a shell would run, so it is the opposite of inert.
550
+ * - **A `catastrophic` verdict changes nothing.** §4.2 withdraws the sticky grants there; it says
551
+ * nothing about refusals, and refusing more is never the direction that needs withdrawing.
552
+ * - **`bypass` changes nothing either**, and that is a positive statement rather than a gap. Deny
553
+ * is resolved at step (1) of {@link decideToolApprovalInner}, *before* the `bypass` return, so
554
+ * a recorded refusal is in force at every rung — which is why this does not copy the allow
555
+ * side's `bypass` guard.
556
+ * - **A call naming several hosts gets the host-less entry.** On the allow side that would show a
557
+ * bound the grant does not have; here the entry covers every host of that tool, which is
558
+ * broader than the call and safe in the direction breadth is safe. The menu shows exactly that
559
+ * entry, so the breadth is on screen rather than inferred.
560
+ * - **`run_shell_command` arriving as a TOOL subject gets a tool entry** — a shell call whose
561
+ * `command` argument cannot even be read. On the allow side that entry would auto-approve every
562
+ * future unreadable shell call, which is why it is excluded there; as a refusal it stops the
563
+ * shell tool for the session, and the dialog says so in the words the entry is written in.
564
+ *
565
+ * The one genuine exclusion is an **MCP call whose server could not be attributed**
566
+ * ({@link toolGrantEntry} returns `null`): the grammar's `server` cannot be the empty string, so
567
+ * the entry would be dropped by its own validator and the human would be told a refusal had been
568
+ * recorded when none was. A shell command that normalizes to nothing is excluded for the same
569
+ * reason — an empty `pattern` is not a legal entry.
570
+ */
571
+ private denyEntryFor;
572
+ /**
573
+ * §6 — record the menu's *always reject* choice, for the life of this runner instance.
574
+ *
575
+ * It lands in the same store `approvals.deny` entries are matched from ({@link approvalRuleLists}
576
+ * concatenates the two), so a refusal the human made at the prompt and one they wrote in their
577
+ * config are one list to the matcher and one list to `/approvals`.
578
+ *
579
+ * **Session-lifetime, and there is nothing else to choose.** There is no persisted deny file;
580
+ * whether there should be is a question about a file users live with, not about this prompt. What
581
+ * the surfaces must not do is say otherwise — a confirmation promising a persistence that did not
582
+ * happen is §6's *offered and then refused* with the evidence hidden.
583
+ */
584
+ private recordDenial;
585
+ /**
586
+ * §3.1/§6 — record a human-granted approval at the given scope. `once` remembers nothing.
587
+ * `session` adds the entry to the in-memory store; `always` additionally persists it (falling
588
+ * back to session-only when the file cannot be written).
589
+ *
590
+ * What is recorded was decided by {@link stickyGrantFor} and shown to the human before they
591
+ * answered; this only stamps it with when and at what scope.
137
592
  */
138
593
  private recordApproval;
594
+ /**
595
+ * §4.7.4 — **drop a tool grant the tool has since weakened out from under, with a notice naming
596
+ * the tool, the server and the hint that moved.**
597
+ *
598
+ * The human approved a tool *as annotated*; a tool that re-annotates itself into a more dangerous
599
+ * shape is a different proposition wearing the same name, so the grant is invalidated and the next
600
+ * call prompts again. Only a **trusted** server can produce a weakening — an untrusted server's
601
+ * effective set is the constant fail-closed default (§4.7.1) and cannot move — which is exactly
602
+ * where it matters, since the trusted server is the one whose rug-pull would otherwise ride an
603
+ * existing grant.
604
+ *
605
+ * **Scoped to the call being decided, never a sweep of the store.** A sweep would read every held
606
+ * grant against a source that can only answer for the tools registered right now, so a server that
607
+ * happened to be offline would read as having weakened everything it ever declared — and the
608
+ * grants would be deleted for it.
609
+ *
610
+ * **The scope is every grant that could auto-approve THIS call, which is at most two.** A grant
611
+ * with no `host` imposes no host condition, so it matches a call that carries one; looking up only
612
+ * the entry this call would grant (`host` included) would miss the tool-only grant that is about
613
+ * to auto-approve it, and the weakening would ride straight through — the exact failure §4.7.4
614
+ * exists to stop. The host-bound entry of a DIFFERENT host is deliberately not a candidate: it
615
+ * does not match this call either, so this call's annotations say nothing about it.
616
+ *
617
+ * **Only allow-side grants.** A weakening makes a tool more dangerous, so dropping an *always
618
+ * reject* over one would be the unsafe direction: the reason to withdraw an approval is the reason
619
+ * to keep a refusal.
620
+ */
621
+ private invalidateWeakenedGrants;
139
622
  /**
140
623
  * Event-stream counterpart to {@link processMessages}: drives the agent's typed
141
624
  * {@link AgentStreamEvent} path using the runner's own thread-bound `runConfig`, so a
@@ -151,7 +634,7 @@ export declare class GthAgentRunner {
151
634
  * leaves the graph suspended on a `humanInTheLoopMiddleware` interrupt rather than
152
635
  * completing. This is the event-stream counterpart to the readline path's
153
636
  * {@link resolveToolInterrupts}: it drains any pending interrupts through
154
- * {@link decideToolApproval} (allow-list → judge → bridged human prompt), resumes via
637
+ * {@link decideToolApproval} (bypass → allow-list → rater → bridged human prompt), resumes via
155
638
  * `streamWithEventsResume({ decisions })`, and loops until the graph completes with no
156
639
  * pending interrupts — so the executed command's output renders into the TUI. Without
157
640
  * this the TUI silently finalized an empty turn (approval gate was dead code on the
@@ -163,15 +646,18 @@ export declare class GthAgentRunner {
163
646
  * resolve any tool-approval interrupts it suspended on, yielding the resumed run's typed
164
647
  * {@link AgentStreamEvent}s so the renderer (the Ink TUI) shows the executed command's
165
648
  * output. Each pending tool call is consulted via {@link decideToolApproval} — the SAME
166
- * three-layer gate the readline path uses (allow-list auto-approve → EXT-10 judge
649
+ * gate the readline path uses (bypass → allow-list approve → CFG-26 AI rater
167
650
  * bridged human callback, defaulting to REJECT when no handler is wired) — and the
168
651
  * collected decisions are sent back via `streamWithEventsResume` as a LangChain HITL
169
652
  * resume (`{ decisions }`). Because a resumed run can suspend again on the next gated
170
653
  * tool call, this loops until the graph completes with no pending interrupts.
171
654
  *
172
655
  * No-ops (yields nothing) when the agent does not support interrupts
173
- * (`getPendingToolInterrupts`/`streamWithEventsResume` absent), so the lean agent and
174
- * non-HITL configs are unaffected. Aborts (`signal`) propagate through the resumed stream.
656
+ * (`getPendingToolInterrupts`/`streamWithEventsResume` absent) that is the only exemption.
657
+ * As of EXT-52 BOTH backends gate `run_shell_command` and expose the interrupt surface, so the
658
+ * lean (default) agent is now exactly the agent this loop serves; only an agent implementation
659
+ * without those methods (e.g. a test double) skips it. Aborts (`signal`) propagate through the
660
+ * resumed stream.
175
661
  */
176
662
  private resolveToolInterruptsWithEvents;
177
663
  getAgent(): GthAgentInterface | null;