muonroi-cli 1.7.2 → 1.8.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 (322) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  4. package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
  7. package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
  8. package/dist/packages/agent-harness-core/src/selector.js +2 -2
  9. package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
  10. package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
  11. package/dist/packages/agent-harness-opentui/src/install.js +1 -1
  12. package/dist/src/__test-stubs__/vitest-setup.js +4 -0
  13. package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
  14. package/dist/src/agent-harness/mock-model.d.ts +1 -1
  15. package/dist/src/agent-harness/mock-model.js +32 -3
  16. package/dist/src/agent-harness/test-spawn.js +3 -1
  17. package/dist/src/chat/chat-keychain.js +9 -1
  18. package/dist/src/cli/cost-forensics.js +12 -12
  19. package/dist/src/cli/keys.d.ts +2 -2
  20. package/dist/src/cli/keys.js +107 -28
  21. package/dist/src/cli/usage-report.js +2 -2
  22. package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
  23. package/dist/src/council/__tests__/abort-threading.test.js +193 -0
  24. package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
  25. package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
  26. package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
  27. package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
  28. package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
  29. package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
  30. package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
  31. package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
  32. package/dist/src/council/__tests__/research-tools.test.js +4 -0
  33. package/dist/src/council/__tests__/round-tools.test.js +6 -0
  34. package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
  35. package/dist/src/council/clarifier-question-cap.test.js +73 -0
  36. package/dist/src/council/clarifier.d.ts +29 -1
  37. package/dist/src/council/clarifier.js +64 -1
  38. package/dist/src/council/debate-planner.d.ts +2 -1
  39. package/dist/src/council/debate-planner.js +5 -3
  40. package/dist/src/council/debate.d.ts +9 -0
  41. package/dist/src/council/debate.js +88 -8
  42. package/dist/src/council/index.d.ts +46 -1
  43. package/dist/src/council/index.js +125 -19
  44. package/dist/src/council/llm.js +41 -13
  45. package/dist/src/council/prompts.js +56 -21
  46. package/dist/src/council/types.d.ts +14 -1
  47. package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
  48. package/dist/src/ee/bridge.d.ts +3 -1
  49. package/dist/src/ee/bridge.js +9 -3
  50. package/dist/src/ee/bridge.test.js +1 -2
  51. package/dist/src/ee/client.js +15 -2
  52. package/dist/src/ee/recall-ledger.d.ts +20 -0
  53. package/dist/src/ee/recall-ledger.js +40 -1
  54. package/dist/src/ee/search.d.ts +25 -0
  55. package/dist/src/ee/search.js +73 -0
  56. package/dist/src/ee/types.d.ts +15 -1
  57. package/dist/src/ee/who-am-i.d.ts +41 -0
  58. package/dist/src/ee/who-am-i.js +143 -0
  59. package/dist/src/ee/who-am-i.test.d.ts +1 -0
  60. package/dist/src/ee/who-am-i.test.js +101 -0
  61. package/dist/src/generated/version.d.ts +1 -1
  62. package/dist/src/generated/version.js +1 -1
  63. package/dist/src/headless/council-answers.js +1 -0
  64. package/dist/src/headless/output.d.ts +30 -1
  65. package/dist/src/headless/output.js +180 -0
  66. package/dist/src/headless/output.test.js +61 -1
  67. package/dist/src/hooks/index.js +48 -2
  68. package/dist/src/index.d.ts +2 -0
  69. package/dist/src/index.js +55 -60
  70. package/dist/src/lsp/client-normalize.test.d.ts +12 -0
  71. package/dist/src/lsp/client-normalize.test.js +34 -0
  72. package/dist/src/lsp/client.d.ts +1 -0
  73. package/dist/src/lsp/client.js +10 -2
  74. package/dist/src/lsp/manager.d.ts +1 -1
  75. package/dist/src/lsp/manager.js +2 -2
  76. package/dist/src/lsp/manager.test.js +2 -2
  77. package/dist/src/lsp/runtime.d.ts +1 -1
  78. package/dist/src/lsp/runtime.js +2 -2
  79. package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
  80. package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
  81. package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
  82. package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
  83. package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
  84. package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
  85. package/dist/src/mcp/client-pool.d.ts +1 -1
  86. package/dist/src/mcp/client-pool.js +25 -9
  87. package/dist/src/mcp/ee-tools.d.ts +10 -0
  88. package/dist/src/mcp/ee-tools.js +34 -0
  89. package/dist/src/mcp/oauth-callback.js +2 -2
  90. package/dist/src/mcp/parse-headers.test.js +14 -14
  91. package/dist/src/mcp/self-verify-jobs.js +1 -3
  92. package/dist/src/mcp/setup-guide-text.js +74 -74
  93. package/dist/src/mcp/smoke.test.js +43 -43
  94. package/dist/src/models/catalog-gemini.test.js +12 -10
  95. package/dist/src/models/catalog.json +106 -38
  96. package/dist/src/ops/doctor.js +7 -7
  97. package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
  98. package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
  99. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +2 -0
  100. package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
  101. package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
  102. package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
  103. package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
  104. package/dist/src/orchestrator/agent.test.js +5 -2
  105. package/dist/src/orchestrator/auto-commit.d.ts +110 -0
  106. package/dist/src/orchestrator/auto-commit.js +364 -0
  107. package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
  108. package/dist/src/orchestrator/batch-turn-runner.js +2 -1
  109. package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
  110. package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
  111. package/dist/src/orchestrator/compaction.d.ts +22 -0
  112. package/dist/src/orchestrator/compaction.js +42 -0
  113. package/dist/src/orchestrator/delegations.d.ts +1 -0
  114. package/dist/src/orchestrator/delegations.js +41 -0
  115. package/dist/src/orchestrator/error-utils.js +27 -2
  116. package/dist/src/orchestrator/message-processor.d.ts +21 -5
  117. package/dist/src/orchestrator/message-processor.js +219 -2573
  118. package/dist/src/orchestrator/orchestrator.d.ts +8 -2
  119. package/dist/src/orchestrator/orchestrator.js +187 -34
  120. package/dist/src/orchestrator/preprocessor.d.ts +13 -0
  121. package/dist/src/orchestrator/preprocessor.js +145 -0
  122. package/dist/src/orchestrator/prompts.d.ts +8 -0
  123. package/dist/src/orchestrator/prompts.js +76 -60
  124. package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
  125. package/dist/src/orchestrator/safety-askcard.js +74 -0
  126. package/dist/src/orchestrator/sandbox.test.js +8 -5
  127. package/dist/src/orchestrator/scope-ceiling.js +1 -1
  128. package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
  129. package/dist/src/orchestrator/stall-watchdog.js +23 -0
  130. package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
  131. package/dist/src/orchestrator/stream-runner.js +52 -30
  132. package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
  133. package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
  134. package/dist/src/orchestrator/subagent-compactor.js +83 -10
  135. package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
  136. package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
  137. package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
  138. package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
  139. package/dist/src/orchestrator/tool-args-repair.js +1 -1
  140. package/dist/src/orchestrator/tool-engine.d.ts +151 -0
  141. package/dist/src/orchestrator/tool-engine.js +3079 -0
  142. package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -0
  143. package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
  144. package/dist/src/pil/__tests__/discovery.test.js +89 -145
  145. package/dist/src/pil/__tests__/dual-run.test.js +2 -0
  146. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
  147. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  148. package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
  149. package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
  150. package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
  151. package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
  152. package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
  153. package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
  154. package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
  155. package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
  156. package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
  157. package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
  158. package/dist/src/pil/__tests__/pipeline.test.js +11 -8
  159. package/dist/src/pil/__tests__/response-tools.test.js +52 -0
  160. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  161. package/dist/src/pil/agent-operating-contract.js +5 -3
  162. package/dist/src/pil/agent-operating-contract.test.js +2 -2
  163. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  164. package/dist/src/pil/cheap-model-playbook.js +1 -1
  165. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  166. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  167. package/dist/src/pil/config.d.ts +20 -0
  168. package/dist/src/pil/config.js +35 -0
  169. package/dist/src/pil/discovery-types.d.ts +32 -9
  170. package/dist/src/pil/discovery.d.ts +3 -3
  171. package/dist/src/pil/discovery.js +138 -244
  172. package/dist/src/pil/index.d.ts +1 -1
  173. package/dist/src/pil/index.js +1 -1
  174. package/dist/src/pil/layer1-intent.d.ts +9 -1
  175. package/dist/src/pil/layer1-intent.js +99 -44
  176. package/dist/src/pil/layer1-intent.test.js +112 -2
  177. package/dist/src/pil/layer15-context-scan.js +1 -1
  178. package/dist/src/pil/layer16-clarity.d.ts +21 -25
  179. package/dist/src/pil/layer16-clarity.js +52 -88
  180. package/dist/src/pil/layer18-acceptance.d.ts +13 -3
  181. package/dist/src/pil/layer18-acceptance.js +1 -75
  182. package/dist/src/pil/layer2-personality.js +7 -8
  183. package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
  184. package/dist/src/pil/layer2_5-ponytail.js +41 -0
  185. package/dist/src/pil/layer3-ee-injection.js +180 -14
  186. package/dist/src/pil/layer4-gsd.js +22 -8
  187. package/dist/src/pil/layer5-context.d.ts +1 -0
  188. package/dist/src/pil/layer5-context.js +26 -11
  189. package/dist/src/pil/layer6-output.js +23 -5
  190. package/dist/src/pil/llm-classify.js +3 -3
  191. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  192. package/dist/src/pil/native-capabilities-workbook.js +4 -3
  193. package/dist/src/pil/pipeline.js +15 -1
  194. package/dist/src/pil/response-tools.d.ts +16 -0
  195. package/dist/src/pil/response-tools.js +41 -0
  196. package/dist/src/pil/schema.d.ts +8 -0
  197. package/dist/src/pil/schema.js +24 -3
  198. package/dist/src/pil/task-tier-map.js +7 -7
  199. package/dist/src/pil/types.d.ts +9 -3
  200. package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
  201. package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
  202. package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
  203. package/dist/src/product-loop/discovery-interview.d.ts +20 -0
  204. package/dist/src/product-loop/discovery-interview.js +121 -7
  205. package/dist/src/product-loop/done-gate.js +3 -3
  206. package/dist/src/product-loop/gather.js +19 -5
  207. package/dist/src/product-loop/loop-driver.js +20 -20
  208. package/dist/src/product-loop/progress-snapshot.js +4 -4
  209. package/dist/src/product-loop/sprint-self-verify.js +2 -2
  210. package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
  211. package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
  212. package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
  213. package/dist/src/providers/auth/gcloud.d.ts +28 -0
  214. package/dist/src/providers/auth/gcloud.js +102 -0
  215. package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
  216. package/dist/src/providers/auth/gemini-oauth.js +292 -105
  217. package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
  218. package/dist/src/providers/auth/grok-oauth.js +1 -1
  219. package/dist/src/providers/auth/registry.js +28 -6
  220. package/dist/src/providers/gemini.d.ts +1 -1
  221. package/dist/src/providers/gemini.js +10 -3
  222. package/dist/src/providers/keychain.d.ts +2 -2
  223. package/dist/src/providers/keychain.js +20 -2
  224. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  225. package/dist/src/providers/openai.js +5 -1
  226. package/dist/src/providers/runtime.d.ts +1 -1
  227. package/dist/src/providers/runtime.js +11 -2
  228. package/dist/src/providers/siliconflow-sse-repair.js +1 -1
  229. package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
  230. package/dist/src/providers/strategies/google.strategy.js +159 -9
  231. package/dist/src/providers/strategies/thinking-mode.js +19 -6
  232. package/dist/src/providers/types.d.ts +1 -1
  233. package/dist/src/reporter/index.js +1 -1
  234. package/dist/src/router/decide.test.js +40 -8
  235. package/dist/src/router/step-router.d.ts +20 -0
  236. package/dist/src/router/step-router.js +73 -0
  237. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  238. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  239. package/dist/src/scaffold/continuation-prompt.js +60 -60
  240. package/dist/src/scaffold/init-new.js +453 -453
  241. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  242. package/dist/src/self-qa/agentic-loop.js +19 -19
  243. package/dist/src/self-qa/orchestrator.js +5 -5
  244. package/dist/src/self-qa/scenario-planner.js +1 -1
  245. package/dist/src/self-qa/spec-emitter.js +4 -4
  246. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  247. package/dist/src/storage/index.d.ts +1 -1
  248. package/dist/src/storage/index.js +1 -1
  249. package/dist/src/storage/interaction-log.js +5 -5
  250. package/dist/src/storage/migrations.js +122 -122
  251. package/dist/src/storage/session-experience-store.js +4 -4
  252. package/dist/src/storage/sessions.js +43 -43
  253. package/dist/src/storage/transcript-view.js +7 -1
  254. package/dist/src/storage/transcript.d.ts +1 -0
  255. package/dist/src/storage/transcript.js +55 -0
  256. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  257. package/dist/src/storage/usage.js +14 -14
  258. package/dist/src/storage/workspaces.js +12 -12
  259. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  260. package/dist/src/tools/bash.d.ts +36 -23
  261. package/dist/src/tools/bash.js +59 -301
  262. package/dist/src/tools/bash.test.js +2 -257
  263. package/dist/src/tools/file-tracker.d.ts +5 -0
  264. package/dist/src/tools/file-tracker.js +14 -0
  265. package/dist/src/tools/file.js +2 -0
  266. package/dist/src/tools/git-safety.d.ts +38 -1
  267. package/dist/src/tools/git-safety.js +94 -4
  268. package/dist/src/tools/git-safety.test.js +45 -1
  269. package/dist/src/tools/native-tools.d.ts +1 -1
  270. package/dist/src/tools/native-tools.js +56 -0
  271. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  272. package/dist/src/tools/registry-git-safety.test.js +17 -6
  273. package/dist/src/tools/registry.d.ts +5 -0
  274. package/dist/src/tools/registry.js +185 -11
  275. package/dist/src/types/index.d.ts +7 -1
  276. package/dist/src/ui/app.d.ts +1 -0
  277. package/dist/src/ui/app.js +0 -0
  278. package/dist/src/ui/components/council-question-card.js +1 -0
  279. package/dist/src/ui/components/message-view.js +37 -3
  280. package/dist/src/ui/components/structured-response-view.js +7 -0
  281. package/dist/src/ui/components/task-list-panel.js +3 -12
  282. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  283. package/dist/src/ui/containers/chat-feed.js +66 -0
  284. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  285. package/dist/src/ui/containers/modals-layer.js +18 -0
  286. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  287. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  288. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  289. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  290. package/dist/src/ui/slash/council-inspect.js +4 -4
  291. package/dist/src/ui/slash/export.js +16 -1
  292. package/dist/src/ui/slash/menu-items.js +1 -0
  293. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  294. package/dist/src/ui/slash/ponytail.js +19 -0
  295. package/dist/src/ui/status-bar/store.js +0 -1
  296. package/dist/src/ui/status-bar/store.test.js +2 -2
  297. package/dist/src/ui/use-app-logic.d.ts +26 -0
  298. package/dist/src/ui/use-app-logic.js +0 -0
  299. package/dist/src/ui/utils/relaunch.js +1 -1
  300. package/dist/src/ui/utils/text.d.ts +11 -0
  301. package/dist/src/ui/utils/text.js +21 -1
  302. package/dist/src/ui/utils/text.test.js +25 -1
  303. package/dist/src/ui/utils/tools.js +3 -1
  304. package/dist/src/usage/cost-log.js +2 -2
  305. package/dist/src/usage/estimator.d.ts +14 -0
  306. package/dist/src/usage/estimator.js +22 -0
  307. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  308. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  309. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  310. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  311. package/dist/src/utils/clipboard-image.js +23 -23
  312. package/dist/src/utils/llm-deadline.d.ts +9 -1
  313. package/dist/src/utils/llm-deadline.js +31 -2
  314. package/dist/src/utils/permission-mode.d.ts +33 -0
  315. package/dist/src/utils/permission-mode.js +164 -3
  316. package/dist/src/utils/permission-mode.test.js +92 -1
  317. package/dist/src/utils/settings.d.ts +27 -31
  318. package/dist/src/utils/settings.js +100 -109
  319. package/dist/src/utils/side-question.js +2 -2
  320. package/dist/src/utils/skills.js +3 -3
  321. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  322. package/package.json +1 -1
@@ -13,6 +13,28 @@ import { lookupPricing } from "../providers/pricing.js";
13
13
  export function estimateTokensFromChars(chars) {
14
14
  return Math.ceil(chars / 4);
15
15
  }
16
+ /**
17
+ * Sanitize an actual input-token count by comparing it to the estimated
18
+ * (chars/4) count. Some providers (SiliconFlow, possibly others) return
19
+ * implausibly low `prompt_tokens` (e.g. 10) regardless of actual prompt size,
20
+ * which under-reports cost and inflates apparent cache-hit ratios.
21
+ *
22
+ * Rules:
23
+ * - When `actual === undefined`: return `estimated` (no data = use estimate).
24
+ * - When `actual === 0`: return 0 (preserve mock / failed-call semantics).
25
+ * - When `estimated > 0` and `actual < estimated * 0.1`: the value is likely
26
+ * bogus — return `estimated` so cost projections stay accurate.
27
+ * - Otherwise: return `actual` as-is.
28
+ */
29
+ export function sanitizeInputTokens(actual, estimated) {
30
+ if (actual === undefined)
31
+ return estimated;
32
+ if (actual === 0)
33
+ return 0;
34
+ if (estimated > 0 && actual < estimated * 0.1)
35
+ return estimated;
36
+ return actual;
37
+ }
16
38
  /**
17
39
  * Project the USD cost of a request given token estimates and the static pricing table.
18
40
  * Returns 0 for unknown provider/model -- caller decides how to handle (ledger treats as 0 risk).
@@ -0,0 +1,21 @@
1
+ /**
2
+ * src/utils/__tests__/compaction-caps.test.ts
3
+ *
4
+ * G4 regression guard for the token-thrift compaction caps.
5
+ *
6
+ * The compaction caps themselves are wired correctly (cost-leak-b3/b4 specs
7
+ * cover the algorithm; the *-tui specs cover the live wiring). The hole this
8
+ * file closes is DRIFT: nothing pinned the PRODUCTION DEFAULT knob values, and
9
+ * the fast cost-leak specs drive `compactSubAgentMessages` with HARDCODED
10
+ * options (thresholdChars: 60_000, keepLastTurns: 1) that are decoupled from
11
+ * the real getters. So a regression that widens a default (e.g. reverting the
12
+ * Phase C5 40_000 back to 200_000, or fat-fingering a clamp range) would sail
13
+ * past the entire fast suite and only be caught by the slow ~30-120s TUI specs.
14
+ *
15
+ * Part 1 pins each getter's documented contract (default / clamp / honor).
16
+ * Part 2 feeds the REAL getter outputs into the compactor over a synthetic
17
+ * load sized BETWEEN the default threshold and a plausible widened drift, so a
18
+ * default-value regression fails behaviourally too — not just at the value
19
+ * assertion.
20
+ */
21
+ export {};
@@ -0,0 +1,187 @@
1
+ /**
2
+ * src/utils/__tests__/compaction-caps.test.ts
3
+ *
4
+ * G4 regression guard for the token-thrift compaction caps.
5
+ *
6
+ * The compaction caps themselves are wired correctly (cost-leak-b3/b4 specs
7
+ * cover the algorithm; the *-tui specs cover the live wiring). The hole this
8
+ * file closes is DRIFT: nothing pinned the PRODUCTION DEFAULT knob values, and
9
+ * the fast cost-leak specs drive `compactSubAgentMessages` with HARDCODED
10
+ * options (thresholdChars: 60_000, keepLastTurns: 1) that are decoupled from
11
+ * the real getters. So a regression that widens a default (e.g. reverting the
12
+ * Phase C5 40_000 back to 200_000, or fat-fingering a clamp range) would sail
13
+ * past the entire fast suite and only be caught by the slow ~30-120s TUI specs.
14
+ *
15
+ * Part 1 pins each getter's documented contract (default / clamp / honor).
16
+ * Part 2 feeds the REAL getter outputs into the compactor over a synthetic
17
+ * load sized BETWEEN the default threshold and a plausible widened drift, so a
18
+ * default-value regression fails behaviourally too — not just at the value
19
+ * assertion.
20
+ */
21
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
22
+ import { compactSubAgentMessages, cumulativeMessageChars } from "../../orchestrator/subagent-compactor.js";
23
+ import { getSubAgentBudgetChars, getSubAgentCompactKeepLast, getSubAgentCompactThresholdChars, getTopLevelCompactKeepLast, getTopLevelCompactThresholdChars, getTopLevelToolBudgetChars, loadUserSettings, } from "../settings.js";
24
+ const ENV_KEYS = [
25
+ "MUONROI_SUB_AGENT_BUDGET_CHARS",
26
+ "MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS",
27
+ "MUONROI_SUBAGENT_COMPACT_KEEP_LAST",
28
+ "MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS",
29
+ "MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST",
30
+ "MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS",
31
+ ];
32
+ const saved = {};
33
+ beforeEach(() => {
34
+ for (const k of ENV_KEYS) {
35
+ saved[k] = process.env[k];
36
+ delete process.env[k];
37
+ }
38
+ });
39
+ afterEach(() => {
40
+ for (const k of ENV_KEYS) {
41
+ if (saved[k] === undefined)
42
+ delete process.env[k];
43
+ else
44
+ process.env[k] = saved[k];
45
+ }
46
+ });
47
+ describe("compaction cap getters — production contract (G4 drift guard)", () => {
48
+ describe("compact thresholds & keepLast (Phase C5 defaults — no user-settings layer)", () => {
49
+ it("return the documented defaults when env is unset", () => {
50
+ expect(getSubAgentCompactThresholdChars()).toBe(40_000);
51
+ expect(getSubAgentCompactKeepLast()).toBe(3);
52
+ expect(getTopLevelCompactThresholdChars()).toBe(100_000);
53
+ expect(getTopLevelCompactKeepLast()).toBe(5);
54
+ });
55
+ it("clamp out-of-range env back to the default", () => {
56
+ process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "999999999"; // > 500_000
57
+ expect(getSubAgentCompactThresholdChars()).toBe(40_000);
58
+ process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "1000"; // < 20_000
59
+ expect(getSubAgentCompactThresholdChars()).toBe(40_000);
60
+ process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "0"; // < 1
61
+ expect(getSubAgentCompactKeepLast()).toBe(3);
62
+ process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "99"; // > 20
63
+ expect(getSubAgentCompactKeepLast()).toBe(3);
64
+ process.env.MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS = "10000"; // < 50_000
65
+ expect(getTopLevelCompactThresholdChars()).toBe(100_000);
66
+ process.env.MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST = "99"; // > 30
67
+ expect(getTopLevelCompactKeepLast()).toBe(5);
68
+ });
69
+ it("honor valid in-range env overrides", () => {
70
+ process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "60000";
71
+ expect(getSubAgentCompactThresholdChars()).toBe(60_000);
72
+ process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "5";
73
+ expect(getSubAgentCompactKeepLast()).toBe(5);
74
+ process.env.MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS = "250000";
75
+ expect(getTopLevelCompactThresholdChars()).toBe(250_000);
76
+ process.env.MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST = "8";
77
+ expect(getTopLevelCompactKeepLast()).toBe(8);
78
+ });
79
+ });
80
+ describe("budget getters (env override → user-settings → default)", () => {
81
+ it("getSubAgentBudgetChars: default 120_000 when env unset and no user override", () => {
82
+ // Deterministic on a clean checkout / CI (no user-settings.json). When a
83
+ // dev machine HAS an explicit override the getter must honor it instead —
84
+ // the default-drift case is what CI guards.
85
+ const override = loadUserSettings().subAgentBudgetChars;
86
+ if (typeof override === "number" && override >= 20_000 && override <= 600_000) {
87
+ expect(getSubAgentBudgetChars()).toBe(Math.floor(override));
88
+ }
89
+ else {
90
+ expect(getSubAgentBudgetChars()).toBe(120_000);
91
+ }
92
+ });
93
+ it("getTopLevelToolBudgetChars: default 200_000 when env unset and no user override", () => {
94
+ const override = loadUserSettings().topLevelToolBudgetChars;
95
+ if (typeof override === "number" && override >= 50_000 && override <= 1_500_000) {
96
+ expect(getTopLevelToolBudgetChars()).toBe(Math.floor(override));
97
+ }
98
+ else {
99
+ expect(getTopLevelToolBudgetChars()).toBe(200_000);
100
+ }
101
+ });
102
+ it("honor valid in-range env overrides (env short-circuits user-settings)", () => {
103
+ process.env.MUONROI_SUB_AGENT_BUDGET_CHARS = "300000";
104
+ expect(getSubAgentBudgetChars()).toBe(300_000);
105
+ process.env.MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS = "800000";
106
+ expect(getTopLevelToolBudgetChars()).toBe(800_000);
107
+ });
108
+ it("reject out-of-range env (result stays within the documented range)", () => {
109
+ process.env.MUONROI_SUB_AGENT_BUDGET_CHARS = "999999999"; // > 600_000
110
+ const sub = getSubAgentBudgetChars();
111
+ expect(sub).not.toBe(999_999_999);
112
+ expect(sub).toBeGreaterThanOrEqual(20_000);
113
+ expect(sub).toBeLessThanOrEqual(600_000);
114
+ process.env.MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS = "1000"; // < 50_000
115
+ const top = getTopLevelToolBudgetChars();
116
+ expect(top).not.toBe(1000);
117
+ expect(top).toBeGreaterThanOrEqual(50_000);
118
+ expect(top).toBeLessThanOrEqual(1_500_000);
119
+ });
120
+ });
121
+ });
122
+ /**
123
+ * Build N tool turns (assistant tool-call + tool result), each result
124
+ * `resultChars` long, using a tool name that is NOT high-value (so the
125
+ * compactor is allowed to elide it). Mirrors the cost-leak-b3 message shape.
126
+ */
127
+ function buildToolTurns(n, resultChars) {
128
+ const msgs = [
129
+ { role: "system", content: "You are the Explore sub-agent." },
130
+ { role: "user", content: "trace auth wiring" },
131
+ ];
132
+ for (let i = 0; i < n; i++) {
133
+ const id = `c${i}`;
134
+ msgs.push({
135
+ role: "assistant",
136
+ content: [{ type: "tool-call", toolCallId: id, toolName: "fake_read", input: { path: `/tmp/${id}.txt` } }],
137
+ });
138
+ msgs.push({
139
+ role: "tool",
140
+ content: [
141
+ {
142
+ type: "tool-result",
143
+ toolCallId: id,
144
+ toolName: "fake_read",
145
+ output: { type: "text", value: "x".repeat(resultChars) },
146
+ },
147
+ ],
148
+ });
149
+ }
150
+ return msgs;
151
+ }
152
+ describe("compaction fires with the REAL default getter values (G4 behavioural guard)", () => {
153
+ // Load sized BETWEEN the default thresholds (40K sub / 100K top) and a
154
+ // plausible widened drift (200K). So: with the real defaults it compacts;
155
+ // if a default were reverted to 200_000 the load would fall below threshold
156
+ // and these assertions would fail — catching drift behaviourally.
157
+ it("sub-agent: default threshold (40K) elides older tool results on a ~120K load", () => {
158
+ const messages = buildToolTurns(6, 20_000); // ~120K cumulative, 6 tool turns
159
+ const cumulative = cumulativeMessageChars(messages);
160
+ expect(cumulative).toBeGreaterThan(40_000);
161
+ expect(cumulative).toBeLessThan(200_000);
162
+ const compacted = compactSubAgentMessages(messages, {
163
+ thresholdChars: getSubAgentCompactThresholdChars(),
164
+ keepLastTurns: getSubAgentCompactKeepLast(),
165
+ });
166
+ expect(compacted).not.toBe(messages); // identity differs → compaction happened
167
+ const before = JSON.stringify(messages).length;
168
+ const after = JSON.stringify(compacted).length;
169
+ expect(after).toBeLessThan(before);
170
+ expect(JSON.stringify(compacted)).toContain("elided by sub-agent compactor");
171
+ });
172
+ it("top-level: default threshold (100K) elides older tool results on a ~160K load", () => {
173
+ const messages = buildToolTurns(8, 20_000); // ~160K cumulative, 8 tool turns
174
+ const cumulative = cumulativeMessageChars(messages);
175
+ expect(cumulative).toBeGreaterThan(100_000);
176
+ expect(cumulative).toBeLessThan(200_000);
177
+ const compacted = compactSubAgentMessages(messages, {
178
+ thresholdChars: getTopLevelCompactThresholdChars(),
179
+ keepLastTurns: getTopLevelCompactKeepLast(),
180
+ label: "top-level",
181
+ });
182
+ expect(compacted).not.toBe(messages);
183
+ expect(JSON.stringify(compacted).length).toBeLessThan(JSON.stringify(messages).length);
184
+ expect(JSON.stringify(compacted)).toContain("elided by top-level compactor");
185
+ });
186
+ });
187
+ //# sourceMappingURL=compaction-caps.test.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * withDeadlineRace abort-grace: a provider that ignores its abortSignal mid-call
3
+ * must not keep the caller blocked until the (minutes-long) wall-clock deadline.
4
+ * When the user-abort signal fires, the race rejects within a short grace window.
5
+ */
6
+ import { describe, expect, it } from "vitest";
7
+ import { withDeadlineRace } from "../llm-deadline.js";
8
+ describe("withDeadlineRace — abort grace", () => {
9
+ it("rejects shortly after abort even when fn() never settles", async () => {
10
+ const ac = new AbortController();
11
+ const neverSettles = () => new Promise(() => { }); // simulates a stalled provider
12
+ const start = Date.now();
13
+ setTimeout(() => ac.abort(new Error("user pressed Esc")), 40);
14
+ await expect(withDeadlineRace(neverSettles, 60_000, "test", ac.signal, 80)).rejects.toThrow(/aborted by user/);
15
+ // Must unblock far before the 60s deadline.
16
+ expect(Date.now() - start).toBeLessThan(2_000);
17
+ });
18
+ it("rejects immediately when the signal is already aborted", async () => {
19
+ const ac = new AbortController();
20
+ ac.abort(new Error("already cancelled"));
21
+ const neverSettles = () => new Promise(() => { });
22
+ await expect(withDeadlineRace(neverSettles, 60_000, "test", ac.signal, 50)).rejects.toThrow(/aborted by user/);
23
+ });
24
+ it("returns the fn() result when it settles before any abort", async () => {
25
+ const ac = new AbortController();
26
+ await expect(withDeadlineRace(() => Promise.resolve("ok"), 60_000, "test", ac.signal, 80)).resolves.toBe("ok");
27
+ });
28
+ it("still enforces the wall-clock deadline when no signal is passed (back-compat)", async () => {
29
+ const neverSettles = () => new Promise(() => { });
30
+ await expect(withDeadlineRace(neverSettles, 60, "test")).rejects.toThrow(/deadline/);
31
+ });
32
+ });
33
+ //# sourceMappingURL=llm-deadline-abort.test.js.map
@@ -25,24 +25,24 @@ function readWin32() {
25
25
  try {
26
26
  const escaped = tmpFile.replace(/\\/g, "\\\\");
27
27
  // Run in STA thread with retry — clipboard can be locked by other processes
28
- const ps = `
29
- Add-Type -AssemblyName System.Windows.Forms
30
- Add-Type -AssemblyName System.Drawing
31
- $maxRetries = 3
32
- $img = $null
33
- for ($i = 0; $i -lt $maxRetries; $i++) {
34
- try {
35
- $img = [System.Windows.Forms.Clipboard]::GetImage()
36
- if ($img -ne $null) { break }
37
- } catch {
38
- # clipboard locked — wait and retry
39
- }
40
- Start-Sleep -Milliseconds 100
41
- }
42
- if ($img -ne $null) {
43
- $img.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)
44
- $img.Dispose()
45
- }
28
+ const ps = `
29
+ Add-Type -AssemblyName System.Windows.Forms
30
+ Add-Type -AssemblyName System.Drawing
31
+ $maxRetries = 3
32
+ $img = $null
33
+ for ($i = 0; $i -lt $maxRetries; $i++) {
34
+ try {
35
+ $img = [System.Windows.Forms.Clipboard]::GetImage()
36
+ if ($img -ne $null) { break }
37
+ } catch {
38
+ # clipboard locked — wait and retry
39
+ }
40
+ Start-Sleep -Milliseconds 100
41
+ }
42
+ if ($img -ne $null) {
43
+ $img.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)
44
+ $img.Dispose()
45
+ }
46
46
  `;
47
47
  const result = spawnSync("powershell", ["-NoProfile", "-STA", "-Command", ps], { timeout: 8000 });
48
48
  if (result.error || result.status !== 0)
@@ -81,11 +81,11 @@ function readDarwin() {
81
81
  const pngpaste = spawnSync("pngpaste", [tmpFile], { timeout: 5000 });
82
82
  if (pngpaste.status !== 0) {
83
83
  spawnSync("screencapture", ["-c", "-x"]);
84
- const osascript = `
85
- set imgData to the clipboard as «class PNGf»
86
- set f to open for access POSIX file "${tmpFile}" with write permission
87
- write imgData to f
88
- close access f
84
+ const osascript = `
85
+ set imgData to the clipboard as «class PNGf»
86
+ set f to open for access POSIX file "${tmpFile}" with write permission
87
+ write imgData to f
88
+ close access f
89
89
  `;
90
90
  spawnSync("osascript", ["-e", osascript], { timeout: 5000 });
91
91
  }
@@ -41,4 +41,12 @@ export declare function withTimeoutSignal(parent: AbortSignal | undefined, timeo
41
41
  * still be aborted via `withTimeoutSignal` for cleanup; this layer just ensures
42
42
  * the caller is never blocked past the deadline.
43
43
  */
44
- export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: number, label: string): Promise<T>;
44
+ export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: number, label: string,
45
+ /**
46
+ * User-abort signal. When it fires, the race rejects within `abortGraceMs`
47
+ * even if `fn()` hasn't settled — so a provider that ignores its abortSignal
48
+ * mid-call (observed with DeepSeek/grok socket stalls) can't keep the caller
49
+ * (and, for council, the locked composer) blocked until the full `deadlineMs`.
50
+ * The short grace lets the normal fetch-level abort win first when it's quick.
51
+ */
52
+ abortSignal?: AbortSignal, abortGraceMs?: number): Promise<T>;
@@ -62,19 +62,48 @@ export function withTimeoutSignal(parent, timeoutMs) {
62
62
  * still be aborted via `withTimeoutSignal` for cleanup; this layer just ensures
63
63
  * the caller is never blocked past the deadline.
64
64
  */
65
- export async function withDeadlineRace(fn, deadlineMs, label) {
65
+ export async function withDeadlineRace(fn, deadlineMs, label,
66
+ /**
67
+ * User-abort signal. When it fires, the race rejects within `abortGraceMs`
68
+ * even if `fn()` hasn't settled — so a provider that ignores its abortSignal
69
+ * mid-call (observed with DeepSeek/grok socket stalls) can't keep the caller
70
+ * (and, for council, the locked composer) blocked until the full `deadlineMs`.
71
+ * The short grace lets the normal fetch-level abort win first when it's quick.
72
+ */
73
+ abortSignal, abortGraceMs = 1500) {
66
74
  let timer = null;
75
+ let abortTimer = null;
76
+ let abortListener = null;
67
77
  const deadline = new Promise((_, reject) => {
68
78
  timer = setTimeout(() => {
69
79
  reject(new Error(`${label} exceeded ${deadlineMs}ms deadline (timeout)`));
70
80
  }, deadlineMs);
71
81
  });
82
+ const racers = [fn(), deadline];
83
+ if (abortSignal) {
84
+ const abortRace = new Promise((_, reject) => {
85
+ const arm = () => {
86
+ abortTimer = setTimeout(() => reject(new Error(`${label} aborted by user`)), abortGraceMs);
87
+ };
88
+ if (abortSignal.aborted)
89
+ arm();
90
+ else {
91
+ abortListener = arm;
92
+ abortSignal.addEventListener("abort", arm, { once: true });
93
+ }
94
+ });
95
+ racers.push(abortRace);
96
+ }
72
97
  try {
73
- return await Promise.race([fn(), deadline]);
98
+ return await Promise.race(racers);
74
99
  }
75
100
  finally {
76
101
  if (timer)
77
102
  clearTimeout(timer);
103
+ if (abortTimer)
104
+ clearTimeout(abortTimer);
105
+ if (abortListener && abortSignal)
106
+ abortSignal.removeEventListener("abort", abortListener);
78
107
  }
79
108
  }
80
109
  //# sourceMappingURL=llm-deadline.js.map
@@ -5,6 +5,39 @@ export type PermissionMode = "safe" | "auto-edit" | "yolo";
5
5
  * without manual confirmation when the user has opted into auto-edit mode.
6
6
  */
7
7
  export declare const AUTO_EDIT_ALLOWED: ReadonlySet<string>;
8
+ /**
9
+ * Structured block result returned by safety checks.
10
+ * `null` means the command is safe to proceed.
11
+ */
12
+ export interface SafetyBlockResult {
13
+ /** Machine-readable kind for routing to the right askcard layout. */
14
+ kind: "catastrophic" | "dangerous" | "git-safety" | "empty-bash";
15
+ /** Human-readable reason explaining why it was blocked. */
16
+ reason: string;
17
+ /** The original command text that was blocked. */
18
+ command: string;
19
+ }
20
+ /**
21
+ * Returns a SafetyBlockResult if `command` matches a catastrophic
22
+ * pattern, or `null` if the command is safe to proceed.
23
+ *
24
+ * This is checked at the tool layer (BashTool.prepareCommand) and is
25
+ * independent of the UI-layer permission approval system — it applies even
26
+ * in yolo mode.
27
+ */
28
+ export declare function checkCatastrophicCommand(command: string): SafetyBlockResult | null;
29
+ /**
30
+ * Check a command against the "dangerous" patterns used by `toolNeedsApproval`.
31
+ * These are less severe than catastrophic but should still prompt for approval
32
+ * in safe / auto-edit modes.
33
+ *
34
+ * Returns a SafetyBlockResult or null if safe.
35
+ */
36
+ export declare function checkDangerousCommand(command: string): SafetyBlockResult | null;
37
+ /**
38
+ * Check if a command is a safety-blocked git operation (push-on-red, broad staging).
39
+ */
40
+ export declare function checkGitSafetyCommand(command: string, gitSafetyKey: string): SafetyBlockResult | null;
8
41
  /**
9
42
  * Returns true if the given tool call requires manual user approval
10
43
  * under the specified permission mode.
@@ -19,6 +19,159 @@ export const AUTO_EDIT_ALLOWED = new Set([
19
19
  "grep",
20
20
  "list_directory",
21
21
  ]);
22
+ /**
23
+ * Catastrophic commands: unrecoverable operations that are ALWAYS hard-blocked
24
+ * at the tool layer regardless of permission mode (safe/auto-edit/yolo).
25
+ *
26
+ * These are patterns where the potential for irreversible damage to the host
27
+ * system is high enough that no user approval dialog is sufficient — they must
28
+ * never execute. This list intentionally errs on the side of caution.
29
+ *
30
+ * Override: set MUONROI_ALLOW_CATASTROPHIC=1 to bypass (not recommended;
31
+ * intended only for explicit test harness scenarios).
32
+ */
33
+ const CATASTROPHIC_PATTERNS = [
34
+ // Disk / filesystem destruction
35
+ {
36
+ pattern: /\bdd\b[^|&;\n]*\bof=\/dev\//i,
37
+ reason: "dd writing to a raw device can destroy disk contents irreversibly.",
38
+ },
39
+ {
40
+ pattern: /\bmkfs\b/i,
41
+ reason: "mkfs formats a filesystem, destroying all data on the target device.",
42
+ },
43
+ // Fork bomb — classic bash pattern: :(){ :|:& };:
44
+ // Match the defining signature: a function named `:` that calls itself recursively.
45
+ // This is tight enough to avoid false positives on URLs containing `:/`.
46
+ {
47
+ pattern: /:\s*\(\s*\)\s*\{[^}]*:\s*\|\s*:/,
48
+ reason: "Fork bomb pattern detected — would exhaust host process table.",
49
+ },
50
+ // Remote code execution via piped shell
51
+ {
52
+ pattern: /\bcurl\b[^|&;\n]*\|\s*(?:ba)?sh\b/i,
53
+ reason: "curl piped to a shell executes arbitrary remote code without inspection.",
54
+ },
55
+ {
56
+ pattern: /\bwget\b[^|&;\n]*\|\s*(?:ba)?sh\b/i,
57
+ reason: "wget piped to a shell executes arbitrary remote code without inspection.",
58
+ },
59
+ // Privilege escalation
60
+ {
61
+ pattern: /\bsudo\s+/i,
62
+ reason: "sudo privilege escalation is not permitted from the agent shell.",
63
+ },
64
+ {
65
+ pattern: /\bsu\s+-\s*/i,
66
+ reason: "su to root is not permitted from the agent shell.",
67
+ },
68
+ // Persistence mechanisms — block crontab edits/writes; allow only -l (list, read-only)
69
+ // crontab <file>, crontab -, crontab -e all create/modify the crontab.
70
+ {
71
+ pattern: /\bcrontab\b(?!\s+-l\b)/i,
72
+ reason: "crontab write/edit commands create persistent execution outside the session.",
73
+ },
74
+ {
75
+ pattern: /\/etc\/(?:rc\.d|init\.d|systemd|cron)/i,
76
+ reason: "Writing to system init or cron directories would create persistence.",
77
+ },
78
+ // Reverse shells
79
+ {
80
+ pattern: /\bnc\b[^|&;\n]*(?:-e|-c)\s+/i,
81
+ reason: "nc with -e/-c flag can open a reverse shell.",
82
+ },
83
+ {
84
+ pattern: /\bsocat\b[^|&;\n]*(?:EXEC|SYSTEM):/i,
85
+ reason: "socat with EXEC/SYSTEM can open a reverse shell.",
86
+ },
87
+ {
88
+ pattern: /\/dev\/tcp\//i,
89
+ reason: "Bash /dev/tcp redirect can open a covert reverse shell connection.",
90
+ },
91
+ // Exfiltration of credentials / secrets directories
92
+ {
93
+ pattern: /(?:tar|zip|gzip|rsync|scp|sftp|ftp)\b[^|&;\n]*\.(?:muonroi-cli|ssh|gnupg|aws|kube)/i,
94
+ reason: "Archiving or uploading credential directories is not permitted.",
95
+ },
96
+ ];
97
+ /**
98
+ * Returns a SafetyBlockResult if `command` matches a catastrophic
99
+ * pattern, or `null` if the command is safe to proceed.
100
+ *
101
+ * This is checked at the tool layer (BashTool.prepareCommand) and is
102
+ * independent of the UI-layer permission approval system — it applies even
103
+ * in yolo mode.
104
+ */
105
+ export function checkCatastrophicCommand(command) {
106
+ if (process.env.MUONROI_ALLOW_CATASTROPHIC === "1")
107
+ return null;
108
+ for (const { pattern, reason } of CATASTROPHIC_PATTERNS) {
109
+ if (pattern.test(command)) {
110
+ return { kind: "catastrophic", reason, command };
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ /**
116
+ * Check a command against the "dangerous" patterns used by `toolNeedsApproval`.
117
+ * These are less severe than catastrophic but should still prompt for approval
118
+ * in safe / auto-edit modes.
119
+ *
120
+ * Returns a SafetyBlockResult or null if safe.
121
+ */
122
+ export function checkDangerousCommand(command) {
123
+ const dangerousPatterns = [
124
+ // Filesystem destruction
125
+ /rm\s+-rf?\s+\/(?!tmp|var\/tmp)/i,
126
+ // Unrestricted chmod
127
+ /chmod\s+(?:777|a\+[rwx])/i,
128
+ // Code injection
129
+ /eval\s*\(/i,
130
+ /exec\s*\(/i,
131
+ // External network fetches (non-local)
132
+ /curl\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
133
+ /wget\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
134
+ // Process substitution with external commands
135
+ /\$\([^)]*(?:curl|wget|nc|socat)[^)]*\)/i,
136
+ // Writing to /etc (besides allowed subdirs)
137
+ /\bchown\s+root/i,
138
+ ];
139
+ for (const pattern of dangerousPatterns) {
140
+ if (pattern.test(command)) {
141
+ return {
142
+ kind: "dangerous",
143
+ reason: `Command matched dangerous pattern: ${pattern.source}`,
144
+ command,
145
+ };
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+ /**
151
+ * Check if a command is a safety-blocked git operation (push-on-red, broad staging).
152
+ */
153
+ export function checkGitSafetyCommand(command, gitSafetyKey) {
154
+ // Lazy import to avoid circular deps at module level
155
+ const { analyzeGitCommand, checkPushGate, checkSensitiveStaging } = require_inline_safety_deps();
156
+ const gt = analyzeGitCommand(command);
157
+ if (gt.isPush) {
158
+ const gate = checkPushGate(gitSafetyKey);
159
+ if (gate.blocked) {
160
+ return {
161
+ kind: "git-safety",
162
+ reason: `git push blocked: ${gate.failed.join(", ")}`,
163
+ command,
164
+ };
165
+ }
166
+ }
167
+ return null;
168
+ }
169
+ // Inline require to avoid circular dependency at module load.
170
+ function require_inline_safety_deps() {
171
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
172
+ const mod = require("../tools/git-safety.js");
173
+ return mod;
174
+ }
22
175
  /**
23
176
  * Returns true if the given tool call requires manual user approval
24
177
  * under the specified permission mode.
@@ -34,12 +187,20 @@ export function toolNeedsApproval(toolName, mode, context) {
34
187
  if (!cmd)
35
188
  return false;
36
189
  const patterns = [
190
+ // Filesystem destruction
37
191
  /rm\s+-rf?\s+\/(?!tmp|var\/tmp)/i,
38
- /curl\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
39
- /wget\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
40
- /chmod\s+777/i,
192
+ // Unrestricted chmod
193
+ /chmod\s+(?:777|a\+[rwx])/i,
194
+ // Code injection
41
195
  /eval\s*\(/i,
42
196
  /exec\s*\(/i,
197
+ // External network fetches (non-local)
198
+ /curl\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
199
+ /wget\s+https?:\/\/(?!127\.0\.0\.1|localhost)/i,
200
+ // Process substitution with external commands
201
+ /\$\([^)]*(?:curl|wget|nc|socat)[^)]*\)/i,
202
+ // Writing to /etc (besides allowed subdirs)
203
+ /\bchown\s+root/i,
43
204
  ];
44
205
  return patterns.some((p) => p.test(cmd));
45
206
  };