@jaguilar87/gaia 5.0.0-rc.2

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 (621) hide show
  1. package/.claude-plugin/marketplace.json +33 -0
  2. package/.claude-plugin/plugin.json +26 -0
  3. package/ARCHITECTURE.md +335 -0
  4. package/CHANGELOG.md +1298 -0
  5. package/CODE_OF_CONDUCT.md +11 -0
  6. package/CONTRIBUTING.md +146 -0
  7. package/INSTALL.md +436 -0
  8. package/LICENSE +21 -0
  9. package/README.md +222 -0
  10. package/SECURITY.md +47 -0
  11. package/agents/README.md +78 -0
  12. package/agents/cloud-troubleshooter.md +73 -0
  13. package/agents/developer.md +65 -0
  14. package/agents/gaia-operator.md +64 -0
  15. package/agents/gaia-orchestrator.md +111 -0
  16. package/agents/gaia-planner.md +53 -0
  17. package/agents/gaia-system.md +71 -0
  18. package/agents/gitops-operator.md +61 -0
  19. package/agents/terraform-architect.md +63 -0
  20. package/bin/README.md +106 -0
  21. package/bin/cli/__init__.py +1 -0
  22. package/bin/cli/approvals.py +740 -0
  23. package/bin/cli/cleanup.py +562 -0
  24. package/bin/cli/context.py +283 -0
  25. package/bin/cli/doctor.py +651 -0
  26. package/bin/cli/history.py +305 -0
  27. package/bin/cli/memory.py +483 -0
  28. package/bin/cli/metrics.py +1068 -0
  29. package/bin/cli/plans.py +515 -0
  30. package/bin/cli/status.py +302 -0
  31. package/bin/cli/update.py +382 -0
  32. package/bin/gaia +112 -0
  33. package/bin/gaia-cleanup.js +531 -0
  34. package/bin/gaia-doctor.js +635 -0
  35. package/bin/gaia-evidence +126 -0
  36. package/bin/gaia-history.js +251 -0
  37. package/bin/gaia-metrics.js +1278 -0
  38. package/bin/gaia-review.js +269 -0
  39. package/bin/gaia-scan +44 -0
  40. package/bin/gaia-scan.py +589 -0
  41. package/bin/gaia-skills-diagnose.js +929 -0
  42. package/bin/gaia-status.js +278 -0
  43. package/bin/gaia-uninstall.js +111 -0
  44. package/bin/gaia-update.js +919 -0
  45. package/bin/pre-publish-validate.js +610 -0
  46. package/bin/python-detect.js +60 -0
  47. package/bin/validate-sandbox.sh +601 -0
  48. package/commands/README.md +64 -0
  49. package/commands/gaia.md +37 -0
  50. package/commands/scan-project.md +67 -0
  51. package/config/README.md +71 -0
  52. package/config/cloud/aws.json +134 -0
  53. package/config/cloud/gcp.json +139 -0
  54. package/config/context-contracts.json +158 -0
  55. package/config/crons-schema.md +81 -0
  56. package/config/git_standards.json +72 -0
  57. package/config/surface-routing.json +417 -0
  58. package/config/universal-rules.json +102 -0
  59. package/dist/gaia-ops/.claude-plugin/plugin.json +24 -0
  60. package/dist/gaia-ops/README.md +80 -0
  61. package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
  62. package/dist/gaia-ops/agents/developer.md +65 -0
  63. package/dist/gaia-ops/agents/gaia-operator.md +64 -0
  64. package/dist/gaia-ops/agents/gaia-orchestrator.md +111 -0
  65. package/dist/gaia-ops/agents/gaia-planner.md +53 -0
  66. package/dist/gaia-ops/agents/gaia-system.md +71 -0
  67. package/dist/gaia-ops/agents/gitops-operator.md +61 -0
  68. package/dist/gaia-ops/agents/terraform-architect.md +63 -0
  69. package/dist/gaia-ops/commands/gaia.md +37 -0
  70. package/dist/gaia-ops/config/README.md +71 -0
  71. package/dist/gaia-ops/config/cloud/aws.json +134 -0
  72. package/dist/gaia-ops/config/cloud/gcp.json +139 -0
  73. package/dist/gaia-ops/config/context-contracts.json +158 -0
  74. package/dist/gaia-ops/config/crons-schema.md +81 -0
  75. package/dist/gaia-ops/config/git_standards.json +72 -0
  76. package/dist/gaia-ops/config/surface-routing.json +417 -0
  77. package/dist/gaia-ops/config/universal-rules.json +102 -0
  78. package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
  79. package/dist/gaia-ops/hooks/adapters/base.py +219 -0
  80. package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
  81. package/dist/gaia-ops/hooks/adapters/claude_code.py +1890 -0
  82. package/dist/gaia-ops/hooks/adapters/types.py +194 -0
  83. package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
  84. package/dist/gaia-ops/hooks/hooks.json +192 -0
  85. package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
  86. package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
  87. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
  88. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
  89. package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +120 -0
  90. package/dist/gaia-ops/hooks/modules/agents/state_tracker.py +267 -0
  91. package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
  92. package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
  93. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
  94. package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
  95. package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
  96. package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
  97. package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
  98. package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +611 -0
  99. package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
  100. package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
  101. package/dist/gaia-ops/hooks/modules/context/agentic_loop_detector.py +165 -0
  102. package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
  103. package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +218 -0
  104. package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
  105. package/dist/gaia-ops/hooks/modules/context/context_injector.py +558 -0
  106. package/dist/gaia-ops/hooks/modules/context/context_writer.py +530 -0
  107. package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
  108. package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
  109. package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
  110. package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
  111. package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
  112. package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +577 -0
  113. package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
  114. package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
  115. package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
  116. package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
  117. package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
  118. package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +216 -0
  119. package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
  120. package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +122 -0
  121. package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
  122. package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
  123. package/dist/gaia-ops/hooks/modules/security/__init__.py +120 -0
  124. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
  125. package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
  126. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +1638 -0
  127. package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
  128. package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +222 -0
  129. package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +595 -0
  130. package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +87 -0
  131. package/dist/gaia-ops/hooks/modules/security/command_semantics.py +181 -0
  132. package/dist/gaia-ops/hooks/modules/security/composition_rules.py +547 -0
  133. package/dist/gaia-ops/hooks/modules/security/flag_classifiers.py +873 -0
  134. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
  135. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +1131 -0
  136. package/dist/gaia-ops/hooks/modules/security/network_hosts.py +481 -0
  137. package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
  138. package/dist/gaia-ops/hooks/modules/security/shell_unwrapper.py +165 -0
  139. package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
  140. package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
  141. package/dist/gaia-ops/hooks/modules/session/pending_scanner.py +174 -0
  142. package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
  143. package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +160 -0
  144. package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
  145. package/dist/gaia-ops/hooks/modules/session/session_registry.py +333 -0
  146. package/dist/gaia-ops/hooks/modules/tools/__init__.py +29 -0
  147. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +1008 -0
  148. package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  149. package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
  150. package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
  151. package/dist/gaia-ops/hooks/modules/tools/stage_decomposer.py +315 -0
  152. package/dist/gaia-ops/hooks/modules/tools/task_validator.py +294 -0
  153. package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
  154. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
  155. package/dist/gaia-ops/hooks/post_compact.py +43 -0
  156. package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
  157. package/dist/gaia-ops/hooks/pre_compact.py +60 -0
  158. package/dist/gaia-ops/hooks/pre_tool_use.py +413 -0
  159. package/dist/gaia-ops/hooks/session_end_hook.py +77 -0
  160. package/dist/gaia-ops/hooks/session_start.py +81 -0
  161. package/dist/gaia-ops/hooks/stop_hook.py +70 -0
  162. package/dist/gaia-ops/hooks/subagent_start.py +71 -0
  163. package/dist/gaia-ops/hooks/subagent_stop.py +295 -0
  164. package/dist/gaia-ops/hooks/task_completed.py +70 -0
  165. package/dist/gaia-ops/hooks/user_prompt_submit.py +246 -0
  166. package/dist/gaia-ops/settings.json +72 -0
  167. package/dist/gaia-ops/skills/README.md +158 -0
  168. package/dist/gaia-ops/skills/agent-creation/SKILL.md +87 -0
  169. package/dist/gaia-ops/skills/agent-creation/examples.md +170 -0
  170. package/dist/gaia-ops/skills/agent-creation/reference.md +191 -0
  171. package/dist/gaia-ops/skills/agent-protocol/SKILL.md +93 -0
  172. package/dist/gaia-ops/skills/agent-protocol/examples.md +223 -0
  173. package/dist/gaia-ops/skills/agent-response/SKILL.md +69 -0
  174. package/dist/gaia-ops/skills/agentic-loop/SKILL.md +80 -0
  175. package/dist/gaia-ops/skills/agentic-loop/reference.md +378 -0
  176. package/dist/gaia-ops/skills/blog-writing/SKILL.md +98 -0
  177. package/dist/gaia-ops/skills/blog-writing/reference.md +130 -0
  178. package/dist/gaia-ops/skills/brief-spec/SKILL.md +185 -0
  179. package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
  180. package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
  181. package/dist/gaia-ops/skills/context-updater/SKILL.md +87 -0
  182. package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
  183. package/dist/gaia-ops/skills/developer-patterns/SKILL.md +50 -0
  184. package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
  185. package/dist/gaia-ops/skills/execution/SKILL.md +99 -0
  186. package/dist/gaia-ops/skills/fast-queries/SKILL.md +43 -0
  187. package/dist/gaia-ops/skills/gaia-compact/SKILL.md +74 -0
  188. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +108 -0
  189. package/dist/gaia-ops/skills/gaia-patterns/reference.md +395 -0
  190. package/dist/gaia-ops/skills/gaia-planner/SKILL.md +37 -0
  191. package/dist/gaia-ops/skills/gaia-planner/reference.md +107 -0
  192. package/dist/gaia-ops/skills/gaia-release/SKILL.md +85 -0
  193. package/dist/gaia-ops/skills/gaia-release/reference.md +92 -0
  194. package/dist/gaia-ops/skills/gaia-self-check/SKILL.md +114 -0
  195. package/dist/gaia-ops/skills/gaia-self-check/reference.md +453 -0
  196. package/dist/gaia-ops/skills/gaia-verify/SKILL.md +77 -0
  197. package/dist/gaia-ops/skills/gaia-verify/reference.md +80 -0
  198. package/dist/gaia-ops/skills/git-conventions/SKILL.md +47 -0
  199. package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +60 -0
  200. package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
  201. package/dist/gaia-ops/skills/gmail-policy/SKILL.md +200 -0
  202. package/dist/gaia-ops/skills/gmail-policy/reference.md +150 -0
  203. package/dist/gaia-ops/skills/gmail-triage/SKILL.md +100 -0
  204. package/dist/gaia-ops/skills/gws-setup/SKILL.md +99 -0
  205. package/dist/gaia-ops/skills/gws-setup/reference.md +73 -0
  206. package/dist/gaia-ops/skills/investigation/SKILL.md +100 -0
  207. package/dist/gaia-ops/skills/memory-curation/SKILL.md +83 -0
  208. package/dist/gaia-ops/skills/memory-search/SKILL.md +88 -0
  209. package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +160 -0
  210. package/dist/gaia-ops/skills/orchestrator-approval/reference.md +174 -0
  211. package/dist/gaia-ops/skills/pending-approvals/SKILL.md +72 -0
  212. package/dist/gaia-ops/skills/pending-approvals/reference.md +214 -0
  213. package/dist/gaia-ops/skills/readme-writing/SKILL.md +71 -0
  214. package/dist/gaia-ops/skills/readme-writing/reference.md +188 -0
  215. package/dist/gaia-ops/skills/reference.md +135 -0
  216. package/dist/gaia-ops/skills/request-approval/SKILL.md +140 -0
  217. package/dist/gaia-ops/skills/request-approval/examples.md +140 -0
  218. package/dist/gaia-ops/skills/request-approval/reference.md +57 -0
  219. package/dist/gaia-ops/skills/schedule-task/SKILL.md +64 -0
  220. package/dist/gaia-ops/skills/schedule-task/reference.md +233 -0
  221. package/dist/gaia-ops/skills/security-tiers/SKILL.md +141 -0
  222. package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
  223. package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
  224. package/dist/gaia-ops/skills/session-reflection/SKILL.md +69 -0
  225. package/dist/gaia-ops/skills/skill-creation/SKILL.md +92 -0
  226. package/dist/gaia-ops/skills/skill-creation/reference.md +29 -0
  227. package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +89 -0
  228. package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
  229. package/dist/gaia-ops/tools/__init__.py +9 -0
  230. package/dist/gaia-ops/tools/agentic-loop/decide-status.py +210 -0
  231. package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +106 -0
  232. package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +221 -0
  233. package/dist/gaia-ops/tools/context/README.md +132 -0
  234. package/dist/gaia-ops/tools/context/__init__.py +42 -0
  235. package/dist/gaia-ops/tools/context/_paths.py +20 -0
  236. package/dist/gaia-ops/tools/context/context_provider.py +721 -0
  237. package/dist/gaia-ops/tools/context/context_section_reader.py +342 -0
  238. package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
  239. package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
  240. package/dist/gaia-ops/tools/context/surface_router.py +278 -0
  241. package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
  242. package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
  243. package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  244. package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  245. package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  246. package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  247. package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
  248. package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  249. package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
  250. package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
  251. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
  252. package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
  253. package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
  254. package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
  255. package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +264 -0
  256. package/dist/gaia-ops/tools/memory/README.md +0 -0
  257. package/dist/gaia-ops/tools/memory/__init__.py +20 -0
  258. package/dist/gaia-ops/tools/memory/backfill_fts5.py +107 -0
  259. package/dist/gaia-ops/tools/memory/conflict_detector.py +295 -0
  260. package/dist/gaia-ops/tools/memory/episodic.py +1210 -0
  261. package/dist/gaia-ops/tools/memory/git_invalidator.py +262 -0
  262. package/dist/gaia-ops/tools/memory/paths.py +102 -0
  263. package/dist/gaia-ops/tools/memory/scoring.py +193 -0
  264. package/dist/gaia-ops/tools/memory/search_store.py +375 -0
  265. package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
  266. package/dist/gaia-ops/tools/review/__init__.py +1 -0
  267. package/dist/gaia-ops/tools/review/review_engine.py +157 -0
  268. package/dist/gaia-ops/tools/scan/__init__.py +35 -0
  269. package/dist/gaia-ops/tools/scan/config.py +247 -0
  270. package/dist/gaia-ops/tools/scan/merge.py +212 -0
  271. package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
  272. package/dist/gaia-ops/tools/scan/registry.py +127 -0
  273. package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
  274. package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
  275. package/dist/gaia-ops/tools/scan/scanners/environment.py +349 -0
  276. package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
  277. package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
  278. package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
  279. package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
  280. package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
  281. package/dist/gaia-ops/tools/scan/setup.py +686 -0
  282. package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
  283. package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
  284. package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
  285. package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
  286. package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
  287. package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
  288. package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
  289. package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
  290. package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
  291. package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
  292. package/dist/gaia-ops/tools/scan/ui.py +624 -0
  293. package/dist/gaia-ops/tools/scan/verify.py +270 -0
  294. package/dist/gaia-ops/tools/scan/walk.py +118 -0
  295. package/dist/gaia-ops/tools/scan/workspace.py +85 -0
  296. package/dist/gaia-ops/tools/validation/README.md +244 -0
  297. package/dist/gaia-ops/tools/validation/__init__.py +17 -0
  298. package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
  299. package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
  300. package/dist/gaia-security/.claude-plugin/plugin.json +24 -0
  301. package/dist/gaia-security/README.md +90 -0
  302. package/dist/gaia-security/config/universal-rules.json +102 -0
  303. package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
  304. package/dist/gaia-security/hooks/adapters/base.py +219 -0
  305. package/dist/gaia-security/hooks/adapters/channel.py +17 -0
  306. package/dist/gaia-security/hooks/adapters/claude_code.py +1890 -0
  307. package/dist/gaia-security/hooks/adapters/types.py +194 -0
  308. package/dist/gaia-security/hooks/adapters/utils.py +25 -0
  309. package/dist/gaia-security/hooks/hooks.json +113 -0
  310. package/dist/gaia-security/hooks/modules/__init__.py +15 -0
  311. package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
  312. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
  313. package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
  314. package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +120 -0
  315. package/dist/gaia-security/hooks/modules/agents/state_tracker.py +267 -0
  316. package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
  317. package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
  318. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
  319. package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
  320. package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
  321. package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
  322. package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
  323. package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +611 -0
  324. package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
  325. package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
  326. package/dist/gaia-security/hooks/modules/context/agentic_loop_detector.py +165 -0
  327. package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
  328. package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +218 -0
  329. package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
  330. package/dist/gaia-security/hooks/modules/context/context_injector.py +558 -0
  331. package/dist/gaia-security/hooks/modules/context/context_writer.py +530 -0
  332. package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
  333. package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
  334. package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
  335. package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
  336. package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
  337. package/dist/gaia-security/hooks/modules/core/plugin_setup.py +577 -0
  338. package/dist/gaia-security/hooks/modules/core/state.py +179 -0
  339. package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
  340. package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
  341. package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
  342. package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
  343. package/dist/gaia-security/hooks/modules/memory/episode_writer.py +216 -0
  344. package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
  345. package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +122 -0
  346. package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
  347. package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
  348. package/dist/gaia-security/hooks/modules/security/__init__.py +120 -0
  349. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
  350. package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
  351. package/dist/gaia-security/hooks/modules/security/approval_grants.py +1638 -0
  352. package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
  353. package/dist/gaia-security/hooks/modules/security/approval_scopes.py +222 -0
  354. package/dist/gaia-security/hooks/modules/security/blocked_commands.py +595 -0
  355. package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +87 -0
  356. package/dist/gaia-security/hooks/modules/security/command_semantics.py +181 -0
  357. package/dist/gaia-security/hooks/modules/security/composition_rules.py +547 -0
  358. package/dist/gaia-security/hooks/modules/security/flag_classifiers.py +873 -0
  359. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
  360. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +1131 -0
  361. package/dist/gaia-security/hooks/modules/security/network_hosts.py +481 -0
  362. package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
  363. package/dist/gaia-security/hooks/modules/security/shell_unwrapper.py +165 -0
  364. package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
  365. package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
  366. package/dist/gaia-security/hooks/modules/session/pending_scanner.py +174 -0
  367. package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
  368. package/dist/gaia-security/hooks/modules/session/session_event_injector.py +160 -0
  369. package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
  370. package/dist/gaia-security/hooks/modules/session/session_registry.py +333 -0
  371. package/dist/gaia-security/hooks/modules/tools/__init__.py +29 -0
  372. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +1008 -0
  373. package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  374. package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
  375. package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
  376. package/dist/gaia-security/hooks/modules/tools/stage_decomposer.py +315 -0
  377. package/dist/gaia-security/hooks/modules/tools/task_validator.py +294 -0
  378. package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
  379. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
  380. package/dist/gaia-security/hooks/post_tool_use.py +54 -0
  381. package/dist/gaia-security/hooks/pre_tool_use.py +413 -0
  382. package/dist/gaia-security/hooks/session_end_hook.py +77 -0
  383. package/dist/gaia-security/hooks/session_start.py +81 -0
  384. package/dist/gaia-security/hooks/stop_hook.py +70 -0
  385. package/dist/gaia-security/hooks/user_prompt_submit.py +246 -0
  386. package/dist/gaia-security/settings.json +58 -0
  387. package/git-hooks/commit-msg +41 -0
  388. package/hooks/README.md +100 -0
  389. package/hooks/adapters/__init__.py +52 -0
  390. package/hooks/adapters/base.py +219 -0
  391. package/hooks/adapters/channel.py +17 -0
  392. package/hooks/adapters/claude_code.py +1890 -0
  393. package/hooks/adapters/types.py +194 -0
  394. package/hooks/adapters/utils.py +25 -0
  395. package/hooks/elicitation_result.py +179 -0
  396. package/hooks/hooks.json +84 -0
  397. package/hooks/modules/README.md +189 -0
  398. package/hooks/modules/__init__.py +15 -0
  399. package/hooks/modules/agents/__init__.py +29 -0
  400. package/hooks/modules/agents/contract_validator.py +647 -0
  401. package/hooks/modules/agents/response_contract.py +496 -0
  402. package/hooks/modules/agents/skill_injection_verifier.py +120 -0
  403. package/hooks/modules/agents/state_tracker.py +267 -0
  404. package/hooks/modules/agents/task_info_builder.py +74 -0
  405. package/hooks/modules/agents/transcript_analyzer.py +458 -0
  406. package/hooks/modules/agents/transcript_reader.py +152 -0
  407. package/hooks/modules/audit/__init__.py +28 -0
  408. package/hooks/modules/audit/event_detector.py +168 -0
  409. package/hooks/modules/audit/logger.py +131 -0
  410. package/hooks/modules/audit/metrics.py +134 -0
  411. package/hooks/modules/audit/workflow_auditor.py +611 -0
  412. package/hooks/modules/audit/workflow_recorder.py +296 -0
  413. package/hooks/modules/context/__init__.py +11 -0
  414. package/hooks/modules/context/agentic_loop_detector.py +165 -0
  415. package/hooks/modules/context/anchor_tracker.py +317 -0
  416. package/hooks/modules/context/compact_context_builder.py +218 -0
  417. package/hooks/modules/context/context_freshness.py +145 -0
  418. package/hooks/modules/context/context_injector.py +558 -0
  419. package/hooks/modules/context/context_writer.py +530 -0
  420. package/hooks/modules/context/contracts_loader.py +161 -0
  421. package/hooks/modules/core/__init__.py +40 -0
  422. package/hooks/modules/core/hook_entry.py +78 -0
  423. package/hooks/modules/core/paths.py +160 -0
  424. package/hooks/modules/core/plugin_mode.py +149 -0
  425. package/hooks/modules/core/plugin_setup.py +577 -0
  426. package/hooks/modules/core/state.py +179 -0
  427. package/hooks/modules/core/stdin.py +24 -0
  428. package/hooks/modules/events/__init__.py +1 -0
  429. package/hooks/modules/events/event_writer.py +210 -0
  430. package/hooks/modules/evidence/__init__.py +34 -0
  431. package/hooks/modules/evidence/assertions.py +137 -0
  432. package/hooks/modules/evidence/index_writer.py +57 -0
  433. package/hooks/modules/evidence/loader.py +126 -0
  434. package/hooks/modules/evidence/runner.py +241 -0
  435. package/hooks/modules/memory/__init__.py +8 -0
  436. package/hooks/modules/memory/episode_writer.py +216 -0
  437. package/hooks/modules/orchestrator/__init__.py +1 -0
  438. package/hooks/modules/orchestrator/delegate_mode.py +122 -0
  439. package/hooks/modules/scanning/__init__.py +8 -0
  440. package/hooks/modules/scanning/scan_trigger.py +84 -0
  441. package/hooks/modules/security/__init__.py +120 -0
  442. package/hooks/modules/security/approval_cleanup.py +87 -0
  443. package/hooks/modules/security/approval_constants.py +23 -0
  444. package/hooks/modules/security/approval_grants.py +1638 -0
  445. package/hooks/modules/security/approval_messages.py +71 -0
  446. package/hooks/modules/security/approval_scopes.py +222 -0
  447. package/hooks/modules/security/blocked_commands.py +595 -0
  448. package/hooks/modules/security/blocked_message_formatter.py +87 -0
  449. package/hooks/modules/security/command_semantics.py +181 -0
  450. package/hooks/modules/security/composition_rules.py +547 -0
  451. package/hooks/modules/security/flag_classifiers.py +873 -0
  452. package/hooks/modules/security/gitops_validator.py +179 -0
  453. package/hooks/modules/security/mutative_verbs.py +1131 -0
  454. package/hooks/modules/security/network_hosts.py +481 -0
  455. package/hooks/modules/security/prompt_validator.py +40 -0
  456. package/hooks/modules/security/shell_unwrapper.py +165 -0
  457. package/hooks/modules/security/tiers.py +196 -0
  458. package/hooks/modules/session/__init__.py +10 -0
  459. package/hooks/modules/session/pending_scanner.py +174 -0
  460. package/hooks/modules/session/session_context_writer.py +100 -0
  461. package/hooks/modules/session/session_event_injector.py +160 -0
  462. package/hooks/modules/session/session_manager.py +31 -0
  463. package/hooks/modules/session/session_registry.py +333 -0
  464. package/hooks/modules/tools/__init__.py +29 -0
  465. package/hooks/modules/tools/bash_validator.py +1008 -0
  466. package/hooks/modules/tools/cloud_pipe_validator.py +231 -0
  467. package/hooks/modules/tools/hook_response.py +55 -0
  468. package/hooks/modules/tools/shell_parser.py +227 -0
  469. package/hooks/modules/tools/stage_decomposer.py +315 -0
  470. package/hooks/modules/tools/task_validator.py +294 -0
  471. package/hooks/modules/validation/__init__.py +23 -0
  472. package/hooks/modules/validation/commit_validator.py +380 -0
  473. package/hooks/post_compact.py +43 -0
  474. package/hooks/post_tool_use.py +54 -0
  475. package/hooks/pre_compact.py +60 -0
  476. package/hooks/pre_tool_use.py +413 -0
  477. package/hooks/session_end_hook.py +77 -0
  478. package/hooks/session_start.py +81 -0
  479. package/hooks/stop_hook.py +70 -0
  480. package/hooks/subagent_start.py +71 -0
  481. package/hooks/subagent_stop.py +295 -0
  482. package/hooks/task_completed.py +70 -0
  483. package/hooks/user_prompt_submit.py +246 -0
  484. package/index.js +83 -0
  485. package/package.json +103 -0
  486. package/pyproject.toml +32 -0
  487. package/skills/README.md +158 -0
  488. package/skills/agent-creation/SKILL.md +87 -0
  489. package/skills/agent-creation/examples.md +170 -0
  490. package/skills/agent-creation/reference.md +191 -0
  491. package/skills/agent-protocol/SKILL.md +93 -0
  492. package/skills/agent-protocol/examples.md +223 -0
  493. package/skills/agent-response/SKILL.md +69 -0
  494. package/skills/agentic-loop/SKILL.md +80 -0
  495. package/skills/agentic-loop/reference.md +378 -0
  496. package/skills/blog-writing/SKILL.md +98 -0
  497. package/skills/blog-writing/reference.md +130 -0
  498. package/skills/brief-spec/SKILL.md +185 -0
  499. package/skills/command-execution/SKILL.md +64 -0
  500. package/skills/command-execution/reference.md +83 -0
  501. package/skills/context-updater/SKILL.md +87 -0
  502. package/skills/context-updater/examples.md +71 -0
  503. package/skills/developer-patterns/SKILL.md +50 -0
  504. package/skills/developer-patterns/reference.md +112 -0
  505. package/skills/execution/SKILL.md +99 -0
  506. package/skills/fast-queries/SKILL.md +43 -0
  507. package/skills/gaia-compact/SKILL.md +74 -0
  508. package/skills/gaia-patterns/SKILL.md +108 -0
  509. package/skills/gaia-patterns/reference.md +395 -0
  510. package/skills/gaia-planner/SKILL.md +37 -0
  511. package/skills/gaia-planner/reference.md +107 -0
  512. package/skills/gaia-release/SKILL.md +85 -0
  513. package/skills/gaia-release/reference.md +92 -0
  514. package/skills/gaia-self-check/SKILL.md +114 -0
  515. package/skills/gaia-self-check/reference.md +453 -0
  516. package/skills/gaia-verify/SKILL.md +77 -0
  517. package/skills/gaia-verify/reference.md +80 -0
  518. package/skills/git-conventions/SKILL.md +47 -0
  519. package/skills/gitops-patterns/SKILL.md +60 -0
  520. package/skills/gitops-patterns/reference.md +183 -0
  521. package/skills/gmail-policy/SKILL.md +200 -0
  522. package/skills/gmail-policy/reference.md +150 -0
  523. package/skills/gmail-triage/SKILL.md +100 -0
  524. package/skills/gws-setup/SKILL.md +99 -0
  525. package/skills/gws-setup/reference.md +73 -0
  526. package/skills/investigation/SKILL.md +100 -0
  527. package/skills/memory-curation/SKILL.md +83 -0
  528. package/skills/memory-search/SKILL.md +88 -0
  529. package/skills/orchestrator-approval/SKILL.md +160 -0
  530. package/skills/orchestrator-approval/reference.md +174 -0
  531. package/skills/pending-approvals/SKILL.md +72 -0
  532. package/skills/pending-approvals/reference.md +214 -0
  533. package/skills/readme-writing/SKILL.md +71 -0
  534. package/skills/readme-writing/reference.md +188 -0
  535. package/skills/reference.md +135 -0
  536. package/skills/request-approval/SKILL.md +140 -0
  537. package/skills/request-approval/examples.md +140 -0
  538. package/skills/request-approval/reference.md +57 -0
  539. package/skills/schedule-task/SKILL.md +64 -0
  540. package/skills/schedule-task/reference.md +233 -0
  541. package/skills/security-tiers/SKILL.md +141 -0
  542. package/skills/security-tiers/destructive-commands-reference.md +623 -0
  543. package/skills/security-tiers/reference.md +39 -0
  544. package/skills/session-reflection/SKILL.md +69 -0
  545. package/skills/skill-creation/SKILL.md +92 -0
  546. package/skills/skill-creation/reference.md +29 -0
  547. package/skills/terraform-patterns/SKILL.md +89 -0
  548. package/skills/terraform-patterns/reference.md +93 -0
  549. package/templates/README.md +69 -0
  550. package/templates/managed-settings.template.json +43 -0
  551. package/tools/__init__.py +9 -0
  552. package/tools/agentic-loop/decide-status.py +210 -0
  553. package/tools/agentic-loop/parse-metric.py +106 -0
  554. package/tools/agentic-loop/record-iteration.py +221 -0
  555. package/tools/context/README.md +132 -0
  556. package/tools/context/__init__.py +42 -0
  557. package/tools/context/_paths.py +20 -0
  558. package/tools/context/context_provider.py +721 -0
  559. package/tools/context/context_section_reader.py +342 -0
  560. package/tools/context/deep_merge.py +159 -0
  561. package/tools/context/pending_updates.py +760 -0
  562. package/tools/context/surface_router.py +278 -0
  563. package/tools/fast-queries/README.md +65 -0
  564. package/tools/fast-queries/__init__.py +30 -0
  565. package/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  566. package/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  567. package/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  568. package/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  569. package/tools/fast-queries/run_triage.sh +59 -0
  570. package/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  571. package/tools/gaia_simulator/__init__.py +33 -0
  572. package/tools/gaia_simulator/cli.py +354 -0
  573. package/tools/gaia_simulator/extractor.py +457 -0
  574. package/tools/gaia_simulator/reporter.py +258 -0
  575. package/tools/gaia_simulator/routing_simulator.py +334 -0
  576. package/tools/gaia_simulator/runner.py +539 -0
  577. package/tools/gaia_simulator/skills_mapper.py +264 -0
  578. package/tools/memory/README.md +0 -0
  579. package/tools/memory/__init__.py +20 -0
  580. package/tools/memory/backfill_fts5.py +107 -0
  581. package/tools/memory/conflict_detector.py +295 -0
  582. package/tools/memory/episodic.py +1210 -0
  583. package/tools/memory/git_invalidator.py +262 -0
  584. package/tools/memory/paths.py +102 -0
  585. package/tools/memory/scoring.py +193 -0
  586. package/tools/memory/search_store.py +375 -0
  587. package/tools/persist_transcript_analysis.py +85 -0
  588. package/tools/review/__init__.py +1 -0
  589. package/tools/review/review_engine.py +157 -0
  590. package/tools/scan/__init__.py +35 -0
  591. package/tools/scan/config.py +247 -0
  592. package/tools/scan/merge.py +212 -0
  593. package/tools/scan/orchestrator.py +549 -0
  594. package/tools/scan/registry.py +127 -0
  595. package/tools/scan/scanners/__init__.py +18 -0
  596. package/tools/scan/scanners/base.py +137 -0
  597. package/tools/scan/scanners/environment.py +349 -0
  598. package/tools/scan/scanners/git.py +570 -0
  599. package/tools/scan/scanners/infrastructure.py +875 -0
  600. package/tools/scan/scanners/orchestration.py +600 -0
  601. package/tools/scan/scanners/stack.py +1085 -0
  602. package/tools/scan/scanners/tools.py +260 -0
  603. package/tools/scan/setup.py +686 -0
  604. package/tools/scan/tests/__init__.py +1 -0
  605. package/tools/scan/tests/conftest.py +796 -0
  606. package/tools/scan/tests/test_environment.py +323 -0
  607. package/tools/scan/tests/test_git.py +419 -0
  608. package/tools/scan/tests/test_infrastructure.py +382 -0
  609. package/tools/scan/tests/test_integration.py +920 -0
  610. package/tools/scan/tests/test_merge.py +269 -0
  611. package/tools/scan/tests/test_orchestration.py +304 -0
  612. package/tools/scan/tests/test_stack.py +604 -0
  613. package/tools/scan/tests/test_tools.py +349 -0
  614. package/tools/scan/ui.py +624 -0
  615. package/tools/scan/verify.py +270 -0
  616. package/tools/scan/walk.py +118 -0
  617. package/tools/scan/workspace.py +85 -0
  618. package/tools/validation/README.md +244 -0
  619. package/tools/validation/__init__.py +17 -0
  620. package/tools/validation/approval_gate.py +321 -0
  621. package/tools/validation/validate_skills.py +189 -0
@@ -0,0 +1,1131 @@
1
+ """
2
+ Mutative verb detector for shell commands.
3
+
4
+ Simplified three-category pipeline:
5
+ blocked_commands.py -> BLOCKED (exit 2, permanently denied)
6
+ mutative_verbs.py -> MUTATIVE (needs user approval via nonce)
7
+ everything else -> SAFE (auto-approved by elimination)
8
+
9
+ This module detects MUTATIVE commands by scanning tokens for known verb patterns,
10
+ dangerous flags, and command aliases. If a command is not blocked and not mutative,
11
+ it is safe by elimination -- no allowlist needed.
12
+
13
+ Categories retained internally for verb classification:
14
+ - MUTATIVE: ALL state-modifying verbs (approvable via nonce workflow)
15
+ - SIMULATION: plan, diff, preview, template, validate, lint, etc.
16
+ - READ_ONLY: get, list, describe, show, logs, status, etc.
17
+ """
18
+
19
+ import functools
20
+ import logging
21
+ from dataclasses import dataclass
22
+ from typing import Dict, FrozenSet, List, Tuple, Union
23
+
24
+ from .approval_messages import build_t3_approval_instructions
25
+ from .command_semantics import analyze_command
26
+
27
+ try:
28
+ from .blocked_commands import is_blocked_command as _is_blocked_command
29
+ except ImportError:
30
+ _is_blocked_command = None
31
+ logging.getLogger(__name__).warning(
32
+ "blocked_commands.is_blocked_command not importable; "
33
+ "inline code Layer 1 (shell extraction) disabled"
34
+ )
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ # ============================================================================
40
+ # Category Constants
41
+ # ============================================================================
42
+
43
+ CATEGORY_MUTATIVE = "MUTATIVE"
44
+ CATEGORY_SIMULATION = "SIMULATION"
45
+ CATEGORY_READ_ONLY = "READ_ONLY"
46
+ CATEGORY_UNKNOWN = "UNKNOWN"
47
+
48
+
49
+ # ============================================================================
50
+ # MutativeResult
51
+ # ============================================================================
52
+
53
+ @dataclass(frozen=True)
54
+ class MutativeResult:
55
+ """Structured result of mutative verb detection.
56
+
57
+ Attributes:
58
+ is_mutative: Whether the command is classified as mutative (T3).
59
+ category: Verb category: CATEGORY_MUTATIVE, CATEGORY_SIMULATION,
60
+ CATEGORY_READ_ONLY, or CATEGORY_UNKNOWN.
61
+ verb: The extracted verb (e.g., "delete", "apply", "get").
62
+ dangerous_flags: Tuple of flags that escalate the danger level.
63
+ cli_family: Lightweight CLI family hint (e.g., "k8s", "cloud", "git").
64
+ confidence: Confidence level: "high", "medium", or "low".
65
+ reason: Human-readable explanation of the classification.
66
+ """
67
+ is_mutative: bool = False
68
+ category: str = CATEGORY_UNKNOWN
69
+ verb: str = ""
70
+ dangerous_flags: Tuple[str, ...] = ()
71
+ cli_family: str = "unknown"
72
+ confidence: str = "low"
73
+ reason: str = ""
74
+
75
+
76
+
77
+ # ============================================================================
78
+ # Verb Taxonomy Constants
79
+ # ============================================================================
80
+
81
+ MUTATIVE_VERBS: FrozenSet[str] = frozenset({
82
+ # Creation / addition
83
+ # NOTE: "add" removed -- safe by elimination (e.g., git add is local-only)
84
+ "apply", "create", "put", "insert", "register",
85
+ # Modification
86
+ "update", "patch", "set", "modify", "edit", "configure",
87
+ "replace", "overwrite", "write",
88
+ # Deployment / packaging
89
+ # NOTE: "release" removed -- it is a CLI subcommand group noun in `gh release`,
90
+ # `glab release`, etc. The actual mutative actions (create, delete, edit, upload)
91
+ # are already in MUTATIVE_VERBS. Keeping "release" here causes false positives on
92
+ # `gh release view` and any command with "release" as an argument string.
93
+ "deploy", "install", "upgrade", "downgrade", "publish", "promote",
94
+ # Scaling
95
+ "scale", "resize", "autoscale",
96
+ # Lifecycle
97
+ "start", "restart", "reboot", "reload", "refresh", "resume",
98
+ "uncordon", "unsuspend", "enable", "disable", "suspend", "pause",
99
+ "stop", "shutdown", "halt", "abort",
100
+ # Movement / transfer
101
+ "move", "rename", "copy", "sync",
102
+ "import", "export", "migrate", "transfer",
103
+ # Attachment
104
+ # NOTE: "link" removed -- false positive in shell variable names (e.g., "for link in ...").
105
+ # The `ln` command is already covered as a COMMAND_ALIAS.
106
+ "attach", "bind", "connect", "mount",
107
+ # Execution
108
+ # NOTE: "run" removed -- safe by elimination (e.g., docker run is common dev workflow)
109
+ "exec", "execute", "invoke", "trigger", "send", "reply",
110
+ # Git operations
111
+ # NOTE: "stash" removed -- safe by elimination (local-only operation)
112
+ # NOTE: "commit" removed -- local-only operation, trust system
113
+ "push", "merge", "rebase",
114
+ "rollback",
115
+ # Access control
116
+ "grant", "assign", "revoke",
117
+ # Reconciliation
118
+ "reconcile", "rsync",
119
+ # Deletion / removal (approvable via nonce -- blocked_commands.py catches
120
+ # the truly destructive patterns like "delete namespace", "delete-vpc", etc.)
121
+ "delete", "destroy", "remove", "drop", "purge", "wipe", "clean",
122
+ "trash", "shred", "srm",
123
+ "truncate", "kill", "terminate", "uninstall", "unpublish",
124
+ "drain", "evict", "cordon", "deregister", "detach",
125
+ "disconnect", "unbind", "force-delete", "force-remove", "erase",
126
+ # Collaboration (GitHub/GitLab CLI)
127
+ "comment", "label", "annotate", "approve", "close", "reopen", "tag",
128
+ # Helm-specific
129
+ "uninstall",
130
+ # HTTP methods (e.g., glab api -X POST, gh api -X DELETE)
131
+ "post", "put", "patch",
132
+ })
133
+
134
+ SIMULATION_VERBS: FrozenSet[str] = frozenset({
135
+ "plan", "diff", "preview", "template", "render", "simulate",
136
+ "test", "check", "verify", "lint", "validate", "fmt", "format", "audit",
137
+ })
138
+
139
+ READ_ONLY_VERBS: FrozenSet[str] = frozenset({
140
+ "get", "list", "describe", "show", "read", "view", "inspect",
141
+ "info", "status", "log", "logs", "tail", "head",
142
+ "search", "find", "query", "scan", "fetch", "download",
143
+ "version", "help", "whoami", "which", "explain",
144
+ "top", "stat", "history", "blame", "tree", "shortlog", "reflog",
145
+ "env", "auth", "config", "cluster-info", "api-resources", "ls",
146
+ # Compound subcommands that look mutative after hyphen-split but are read-only
147
+ "merge-base",
148
+ })
149
+
150
+
151
+ # ============================================================================
152
+ # Compound Read-Only Subcommands
153
+ # ============================================================================
154
+ # Full subcommand tokens that must be matched BEFORE the hyphen-split logic.
155
+ # Without this, "merge-base" would be split to "merge" and flagged as MUTATIVE.
156
+
157
+ COMPOUND_READ_ONLY_SUBCOMMANDS: FrozenSet[str] = frozenset({
158
+ "merge-base",
159
+ })
160
+
161
+
162
+ # ============================================================================
163
+ # Git Local-Only Subcommands (early-exit guard)
164
+ # ============================================================================
165
+ # Git subcommands that NEVER mutate remote state. When the base command is
166
+ # "git" and the first non-flag token is one of these, short-circuit to
167
+ # non-mutative. This prevents message body text (after -m) from leaking
168
+ # into the verb scanner and triggering false positives on words like
169
+ # "update", "create", "deploy" inside commit messages.
170
+ #
171
+ # NOT included here (intentionally left to the verb scanner):
172
+ # push -- mutates remote
173
+ # merge -- in MUTATIVE_VERBS (local but destructive merge commit)
174
+ # rebase -- in MUTATIVE_VERBS (rewrites history)
175
+ # tag -- in MUTATIVE_VERBS (creates refs, tag -d deletes)
176
+
177
+ GIT_LOCAL_SAFE_SUBCOMMANDS: FrozenSet[str] = frozenset({
178
+ "commit",
179
+ "stash",
180
+ "add",
181
+ "log",
182
+ "diff",
183
+ "status",
184
+ "branch",
185
+ "checkout",
186
+ "switch",
187
+ "reflog",
188
+ "bisect",
189
+ "blame",
190
+ "show",
191
+ "shortlog",
192
+ "whatchanged",
193
+ "notes",
194
+ "reset", # local-only: modifies local refs/staging, never touches remote
195
+ "revert", # local-only: creates a new commit undoing changes, no remote side effects
196
+ "cherry-pick", # local-only: applies commits from another branch, no remote side effects
197
+ })
198
+
199
+
200
+ # ============================================================================
201
+ # Verb+Flag Overrides (mutative verb downgraded to READ_ONLY by a flag)
202
+ # ============================================================================
203
+ # Map of (cli_family, verb) -> frozenset of flag tokens that override to READ_ONLY.
204
+ # Checked AFTER a mutative verb is found but BEFORE returning the MUTATIVE result.
205
+
206
+ VERB_FLAG_READ_ONLY_OVERRIDES: Dict[Tuple[str, str], FrozenSet[str]] = {
207
+ # "git tag -l" / "git tag --list" is listing, not creating/deleting
208
+ ("git", "tag"): frozenset({"-l", "--list"}),
209
+ }
210
+
211
+
212
+ # ============================================================================
213
+ # CLI-Verb Tier Exceptions (unconditional downgrade from MUTATIVE)
214
+ # ============================================================================
215
+ # Downgrade specific (cli_family, verb) combos to a lower tier regardless of
216
+ # flags. Use only when the API-level semantics of the verb are safe despite
217
+ # the generic verb name being in MUTATIVE_VERBS.
218
+ #
219
+ # Key: (cli_family, verb) — cli_family comes from CLI_FAMILY_LOOKUP above.
220
+ # Value: target category constant (CATEGORY_READ_ONLY or CATEGORY_SIMULATION).
221
+ #
222
+ # This dict is protected by the Write/Edit T3 hook (it lives inside the hooks
223
+ # directory). Modifications require user approval.
224
+
225
+ CLI_VERB_TIER_EXCEPTIONS: Dict[Tuple[str, str], str] = {
226
+ # Gmail API: "modify" only changes labels/flags on messages — it cannot
227
+ # alter message content, send mail, or delete anything. Safe as T0.
228
+ ("workspace", "modify"): CATEGORY_READ_ONLY,
229
+ }
230
+
231
+
232
+ # ============================================================================
233
+ # Inline Code Detection — Language-Agnostic 3-Layer Approach
234
+ # ============================================================================
235
+ # When the base command is a runtime interpreter with an inline code flag
236
+ # (e.g., python3 -c, node -e, ruby -e, perl -e), scan the code string
237
+ # using three layers instead of verb-matching tokens:
238
+ # Layer 1: Extract string literals → check against blocked_commands
239
+ # Layer 2: Universal dangerous API keyword patterns
240
+ # Layer 3: Heuristic safety classification (length, paths, encoding)
241
+ import re as _re
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # CLI → inline-code flag mapping (Step 1a)
245
+ # ---------------------------------------------------------------------------
246
+ _INLINE_CODE_MAP: Dict[str, FrozenSet[str]] = {
247
+ "python": frozenset({"-c"}),
248
+ "python3": frozenset({"-c"}),
249
+ "python3.10": frozenset({"-c"}),
250
+ "python3.11": frozenset({"-c"}),
251
+ "python3.12": frozenset({"-c"}),
252
+ "python3.13": frozenset({"-c"}),
253
+ "node": frozenset({"-e", "--eval"}),
254
+ "ruby": frozenset({"-e"}),
255
+ "perl": frozenset({"-e", "-E"}),
256
+ "php": frozenset({"-r"}),
257
+ "lua": frozenset({"-e"}),
258
+ "rscript": frozenset({"-e"}),
259
+ }
260
+ _INLINE_CODE_CLIS: FrozenSet[str] = frozenset(_INLINE_CODE_MAP.keys())
261
+
262
+ # ---------------------------------------------------------------------------
263
+ # Layer 1: Shell command extraction from string literals
264
+ # ---------------------------------------------------------------------------
265
+ _STRING_LITERAL_RE = _re.compile(r"""(?:['"])((?:[^'"\\\n]|\\.){3,})(?:['"])""")
266
+
267
+
268
+ def _extract_embedded_shell_commands(code: str) -> List[str]:
269
+ """Extract string literals from inline code that may contain shell commands."""
270
+ return [m.group(1) for m in _STRING_LITERAL_RE.finditer(code)]
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # Layer 2: Universal dangerous API keyword patterns (category-based)
275
+ # ---------------------------------------------------------------------------
276
+ _UNIVERSAL_DANGEROUS_PATTERNS: Tuple[Tuple[_re.Pattern, str, str], ...] = (
277
+ # Category: Process Execution
278
+ (_re.compile(r"\b(child_process|subprocess)\b"), "process-module", "PROCESS_EXECUTION"),
279
+ (_re.compile(r"\b(execSync|execFile|execFileSync)\s*\("), "exec-sync", "PROCESS_EXECUTION"),
280
+ (_re.compile(r"\bos\.(system|popen|exec[lv]?[pe]?)\s*\("), "os-exec", "PROCESS_EXECUTION"),
281
+ (_re.compile(r"\b(system|exec)\s*\("), "system-call", "PROCESS_EXECUTION"),
282
+ (_re.compile(r"\bspawn(Sync)?\s*\("), "spawn-call", "PROCESS_EXECUTION"),
283
+ (_re.compile(r"\bPopen\s*\("), "popen-call", "PROCESS_EXECUTION"),
284
+ (_re.compile(r"`[^`]{3,}`"), "backtick-exec", "PROCESS_EXECUTION"),
285
+
286
+ # Category: File Deletion
287
+ (_re.compile(r"\b(os\.remove|os\.unlink|os\.rmdir)\s*\("), "os-delete", "FILE_DELETION"),
288
+ (_re.compile(r"\b(shutil\.rmtree|shutil\.move)\s*\("), "shutil-delete", "FILE_DELETION"),
289
+ (_re.compile(r"\bfs\.(unlink|rmdir|rm)(Sync)?\s*\("), "fs-delete", "FILE_DELETION"),
290
+ # Also match .unlinkSync( / .rmSync( / .rmdirSync( as method calls (e.g., require('fs').unlinkSync())
291
+ (_re.compile(r"\.(unlink|rmdir|rm)(Sync)?\s*\("), "fs-delete", "FILE_DELETION"),
292
+ (_re.compile(r"\bFile\.(delete|unlink)\s*\("), "file-delete", "FILE_DELETION"),
293
+ (_re.compile(r"\bunlink\s*\("), "unlink-call", "FILE_DELETION"),
294
+ (_re.compile(r"\brmtree\s*\("), "rmtree-call", "FILE_DELETION"),
295
+ (_re.compile(r"\bFileUtils\.rm"), "fileutils-rm", "FILE_DELETION"),
296
+ (_re.compile(r"pathlib\.Path\([^)]*\)\.(unlink|rmdir)"), "pathlib-delete", "FILE_DELETION"),
297
+
298
+ # Category: File Write
299
+ (_re.compile(r"open\s*\([^)]*['\"][wWaA]"), "file-write-open", "FILE_WRITE"),
300
+ (_re.compile(r"\bfs\.writeFile(Sync)?\s*\("), "fs-write", "FILE_WRITE"),
301
+ # Also match .writeFileSync( / .appendFileSync( as method calls
302
+ (_re.compile(r"\.writeFile(Sync)?\s*\("), "fs-write", "FILE_WRITE"),
303
+ (_re.compile(r"\bfs\.appendFile(Sync)?\s*\("), "fs-append", "FILE_WRITE"),
304
+ (_re.compile(r"\.appendFile(Sync)?\s*\("), "fs-append", "FILE_WRITE"),
305
+ (_re.compile(r"\bFile\.(write|open)\b.*['\"][wWaA]"), "file-write-ruby", "FILE_WRITE"),
306
+ (_re.compile(r"\.write\s*\("), "file-write", "FILE_WRITE"),
307
+ (_re.compile(r"pathlib\.Path\([^)]*\)\.(rename|write_)"), "pathlib-write", "FILE_WRITE"),
308
+
309
+ # Category: File System Mutation (os.rename, os.makedirs, shutil.copy)
310
+ (_re.compile(r"\bos\.rename\s*\("), "os-rename", "FILE_MUTATION"),
311
+ (_re.compile(r"\bos\.makedirs?\s*\("), "os-makedirs", "FILE_MUTATION"),
312
+ (_re.compile(r"\bshutil\.copy\s*\("), "shutil-copy", "FILE_MUTATION"),
313
+
314
+ # Category: Network
315
+ (_re.compile(r"\bhttps?://\S+"), "url-literal", "NETWORK"),
316
+ (_re.compile(r"\b(fetch|axios|requests\.get|urllib)\s*\("), "http-call", "NETWORK"),
317
+ (_re.compile(r"\bNet::HTTP\b"), "net-http", "NETWORK"),
318
+
319
+ # Category: Permission Modification
320
+ (_re.compile(r"\bos\.chmod\s*\("), "os-chmod", "PERMISSION_MOD"),
321
+ (_re.compile(r"\bfs\.chmod(Sync)?\s*\("), "fs-chmod", "PERMISSION_MOD"),
322
+ )
323
+
324
+ # ---------------------------------------------------------------------------
325
+ # Layer 3: Heuristic safety classification
326
+ # ---------------------------------------------------------------------------
327
+ _SUSPICIOUS_HEURISTICS: Tuple[Tuple[_re.Pattern, str], ...] = (
328
+ (_re.compile(r"\b(base64|b64encode|b64decode|atob|btoa)\b"), "encoding"),
329
+ (_re.compile(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"), "ip-address"),
330
+ )
331
+
332
+ MAX_SAFE_INLINE_LENGTH = 150
333
+ MAX_NORMAL_INLINE_LENGTH = 500
334
+
335
+
336
+ # ============================================================================
337
+ # Command Aliases (single-token commands that map to a category)
338
+ # ============================================================================
339
+
340
+ # All command aliases are MUTATIVE (approvable via nonce).
341
+ # The truly destructive patterns (rm -rf /, dd of=/dev/sda, mkfs, fdisk) are
342
+ # permanently blocked by blocked_commands.py before the verb detector runs.
343
+ COMMAND_ALIASES: Dict[str, str] = {
344
+ "rm": CATEGORY_MUTATIVE,
345
+ "rmdir": CATEGORY_MUTATIVE,
346
+ "mkdir": CATEGORY_MUTATIVE,
347
+ "mv": CATEGORY_MUTATIVE,
348
+ "cp": CATEGORY_MUTATIVE,
349
+ "ln": CATEGORY_MUTATIVE,
350
+ "dd": CATEGORY_MUTATIVE,
351
+ "mkfs": CATEGORY_MUTATIVE,
352
+ "fdisk": CATEGORY_MUTATIVE,
353
+ "chmod": CATEGORY_MUTATIVE,
354
+ "chown": CATEGORY_MUTATIVE,
355
+ "chgrp": CATEGORY_MUTATIVE,
356
+ "nohup": CATEGORY_MUTATIVE,
357
+ }
358
+
359
+
360
+ # ============================================================================
361
+ # Simulation Flags (--dry-run and equivalents)
362
+ # ============================================================================
363
+
364
+ SIMULATION_FLAGS: FrozenSet[str] = frozenset({
365
+ "--dry-run",
366
+ "--dryrun",
367
+ "--dry-run=client",
368
+ "--dry-run=server",
369
+ })
370
+
371
+
372
+ # ============================================================================
373
+ # --help Exemption Whitelist (T3 -> T0 downgrade for well-known CLIs)
374
+ # ============================================================================
375
+ # Only CLIs listed here get the --help exemption in Step 3.5 of the detection
376
+ # algorithm. Command aliases (rm, mv, dd, etc.) are intentionally excluded:
377
+ # they may process arguments before honoring --help, so keeping them T3
378
+ # preserves safety.
379
+
380
+ HELP_FLAGS: FrozenSet[str] = frozenset({"--help", "-h", "-?", "--usage"})
381
+
382
+ # CLI families where `<cli> <verb> --help` is idempotent (exit-and-print).
383
+ # Keys come from CLI_FAMILY_LOOKUP (defined further below in this module).
384
+ HELP_IDEMPOTENT_FAMILIES: FrozenSet[str] = frozenset({
385
+ "k8s", # kubectl, helm, flux, kustomize
386
+ "iac", # terraform, terragrunt, pulumi
387
+ "git", # git subcommands respect --help via man page
388
+ "docker", # docker, podman
389
+ "cloud", # aws, gcloud, gh, glab, az
390
+ "package", # npm, pip, yarn, bun, cargo, brew, apt
391
+ "build", # make, cmake, bazel, gradle, mvn
392
+ "runtime", # node, python, python3
393
+ "linter", # pytest, mypy, ruff, black, flake8
394
+ })
395
+
396
+ # Explicit base_cmd whitelist (not covered by CLI_FAMILY_LOOKUP).
397
+ HELP_IDEMPOTENT_BASE_CMDS: FrozenSet[str] = frozenset({
398
+ "gaia", # project CLI, not in CLI_FAMILY_LOOKUP
399
+ })
400
+
401
+ # Shell redirect tokens (e.g., "2>&1", ">out", "<in") that shlex produces
402
+ # as non-flag tokens but carry no CLI semantic value. Used by the --help
403
+ # exemption to count only real positional args.
404
+ import re as _re_help
405
+ _SHELL_REDIRECT_RE = _re_help.compile(r"^(\d*[<>]&?\d*|[<>]{1,2}.*)$")
406
+
407
+
408
+ # ============================================================================
409
+ # Dangerous Flags (context-sensitive)
410
+ # ============================================================================
411
+
412
+ DANGEROUS_FLAGS: Dict[str, str] = {
413
+ "--force": "ALWAYS",
414
+ "--no-preserve-root": "ALWAYS",
415
+ "--force-with-lease": "ALWAYS",
416
+ "--prune": "ALWAYS",
417
+ "--cascade": "ALWAYS",
418
+ "--grace-period=0": "ALWAYS",
419
+ "--now": "ALWAYS",
420
+ "-f": "CONTEXT",
421
+ "-r": "CONTEXT",
422
+ "-R": "CONTEXT",
423
+ "-D": "CONTEXT",
424
+ "-M": "CONTEXT",
425
+ "--recursive": "CONTEXT",
426
+ "--delete": "CONTEXT",
427
+ "-rf": "ALWAYS",
428
+ "-fr": "ALWAYS",
429
+ }
430
+
431
+ # CLIs where -f means --force (not --file or --format)
432
+ F_FLAG_MEANS_FORCE: FrozenSet[str] = frozenset({
433
+ "rm", "cp", "mv", "ln", "docker", "podman",
434
+ "kubectl", "helm", "apt-get", "brew",
435
+ })
436
+
437
+ # CLIs where -r means recursive delete (not --region or --role)
438
+ R_FLAG_MEANS_RECURSIVE_DELETE: FrozenSet[str] = frozenset({
439
+ "rm", "cp", "chmod", "chown", "chgrp", "find",
440
+ "gsutil",
441
+ })
442
+
443
+ # CLIs where -D means force-delete (not -D for other meanings)
444
+ D_FLAG_MEANS_FORCE_DELETE: FrozenSet[str] = frozenset({
445
+ "git",
446
+ })
447
+
448
+ # CLIs where -M means force-move/rename (not -M for other meanings)
449
+ M_FLAG_MEANS_FORCE_MOVE: FrozenSet[str] = frozenset({
450
+ "git",
451
+ })
452
+
453
+ # CLIs where --delete is a destructive flag (not a query filter)
454
+ DELETE_FLAG_IS_DESTRUCTIVE: FrozenSet[str] = frozenset({
455
+ "git", "rsync",
456
+ })
457
+
458
+
459
+ # ============================================================================
460
+ # Lightweight CLI Family Lookup (metadata only, not routing)
461
+ # ============================================================================
462
+
463
+ CLI_FAMILY_LOOKUP: Dict[str, str] = {
464
+ "kubectl": "k8s", "helm": "k8s", "flux": "k8s", "kustomize": "k8s",
465
+ "k9s": "k8s", "kubectx": "k8s", "kubens": "k8s", "stern": "k8s",
466
+ "terraform": "iac", "terragrunt": "iac", "pulumi": "iac", "cdktf": "iac",
467
+ "git": "git",
468
+ "docker": "docker", "podman": "docker",
469
+ "docker-compose": "docker", "podman-compose": "docker",
470
+ "aws": "cloud", "gcloud": "cloud", "gsutil": "cloud", "az": "cloud",
471
+ "eksctl": "cloud", "gh": "cloud", "glab": "cloud", "gws": "workspace",
472
+ "vercel": "cloud", "netlify": "cloud",
473
+ "fly": "cloud", "flyctl": "cloud", "heroku": "cloud",
474
+ "npm": "package", "npx": "package", "pnpm": "package",
475
+ "yarn": "package", "bun": "package", "deno": "package",
476
+ "pip": "package", "pip3": "package", "poetry": "package",
477
+ "pipenv": "package", "uv": "package",
478
+ "apt": "package", "apt-get": "package", "brew": "package",
479
+ "cargo": "package", "go": "package",
480
+ "make": "build", "cmake": "build", "bazel": "build",
481
+ "gradle": "build", "mvn": "build",
482
+ "node": "runtime", "python": "runtime", "python3": "runtime",
483
+ "tsx": "runtime", "ts-node": "runtime",
484
+ "pytest": "linter", "mypy": "linter", "black": "linter",
485
+ "ruff": "linter", "flake8": "linter", "pylint": "linter",
486
+ "systemctl": "system", "service": "system", "supervisorctl": "system",
487
+ }
488
+
489
+
490
+ # ============================================================================
491
+ # Dangerous Flag Scanning
492
+ # ============================================================================
493
+
494
+ def _scan_dangerous_flags(
495
+ tokens: Union[List[str], tuple],
496
+ cli: str,
497
+ ) -> Tuple[str, ...]:
498
+ """Scan tokens for dangerous flags with context sensitivity.
499
+
500
+ Context rules:
501
+ - "-f" is only dangerous if cli is in F_FLAG_MEANS_FORCE
502
+ - "-r"/"-R" is only dangerous if cli is in R_FLAG_MEANS_RECURSIVE_DELETE
503
+ - "-D" is only dangerous if cli is in D_FLAG_MEANS_FORCE_DELETE
504
+ - "-M" is only dangerous if cli is in M_FLAG_MEANS_FORCE_MOVE
505
+ - "--delete" is only dangerous if cli is in DELETE_FLAG_IS_DESTRUCTIVE
506
+ - Compound flags like "-rf" are always dangerous
507
+
508
+ Args:
509
+ tokens: Tokenized command.
510
+ cli: CLI tool name.
511
+
512
+ Returns:
513
+ Tuple of dangerous flag strings found.
514
+ """
515
+ found: List[str] = []
516
+
517
+ for token in tokens:
518
+ if not token.startswith("-"):
519
+ continue
520
+
521
+ # Check exact matches in DANGEROUS_FLAGS
522
+ if token in DANGEROUS_FLAGS:
523
+ flag_type = DANGEROUS_FLAGS[token]
524
+
525
+ if flag_type == "ALWAYS":
526
+ found.append(token)
527
+ continue
528
+
529
+ # CONTEXT-sensitive flags
530
+ if token == "-f":
531
+ if cli in F_FLAG_MEANS_FORCE:
532
+ found.append(token)
533
+ elif token in ("-r", "-R"):
534
+ if cli in R_FLAG_MEANS_RECURSIVE_DELETE:
535
+ found.append(token)
536
+ elif token == "-D":
537
+ if cli in D_FLAG_MEANS_FORCE_DELETE:
538
+ found.append(token)
539
+ elif token == "-M":
540
+ if cli in M_FLAG_MEANS_FORCE_MOVE:
541
+ found.append(token)
542
+ elif token == "--delete":
543
+ if cli in DELETE_FLAG_IS_DESTRUCTIVE:
544
+ found.append(token)
545
+ elif token == "--recursive":
546
+ if cli in R_FLAG_MEANS_RECURSIVE_DELETE:
547
+ found.append(token)
548
+
549
+ # Check for compound short flags containing dangerous combos
550
+ # e.g., "-rfi" contains both -r and -f
551
+ elif len(token) > 2 and token[0] == "-" and token[1] != "-":
552
+ flag_chars = token[1:]
553
+ if "r" in flag_chars and "f" in flag_chars:
554
+ found.append(token)
555
+ elif "f" in flag_chars and cli in F_FLAG_MEANS_FORCE:
556
+ found.append(token)
557
+ elif "r" in flag_chars and cli in R_FLAG_MEANS_RECURSIVE_DELETE:
558
+ found.append(token)
559
+
560
+ return tuple(found)
561
+
562
+
563
+ # ============================================================================
564
+ # CamelCase Splitting
565
+ # ============================================================================
566
+
567
+ def split_camel_case(token: str) -> List[str]:
568
+ """Split a camelCase token into lowercase component words.
569
+
570
+ Examples:
571
+ "batchDelete" -> ["batch", "delete"]
572
+ "deleteMessages" -> ["delete", "messages"]
573
+ "GET" -> ["get"] (all-caps stays as one token)
574
+ "already_snake" -> ["already_snake"] (no camelCase boundary)
575
+ """
576
+ parts = _re.sub(r"([a-z])([A-Z])", r"\1 \2", token).split()
577
+ return [p.lower() for p in parts] if len(parts) > 1 else [token.lower()]
578
+
579
+
580
+ # ============================================================================
581
+ # Main Detection Function
582
+ # ============================================================================
583
+
584
+ @functools.lru_cache(maxsize=128)
585
+ def detect_mutative_command(command: str) -> MutativeResult:
586
+ """Analyze a shell command and return a structured mutative assessment.
587
+
588
+ Simplified algorithm (CLI-agnostic):
589
+ 1. Tokenize the command.
590
+ 2. COMMAND_ALIASES fast-path.
591
+ 3. Simulation flag override: --dry-run anywhere = not mutative.
592
+ 4. Scan the first semantic non-flag tokens after the base CLI.
593
+ 5. Scan for dangerous flags.
594
+ 6. No match: not mutative (safe by elimination).
595
+
596
+ Args:
597
+ command: Raw shell command string.
598
+
599
+ Returns:
600
+ MutativeResult with full classification details.
601
+ """
602
+ # --- Edge case: empty command ---
603
+ if not command or not command.strip():
604
+ return MutativeResult(
605
+ is_mutative=False,
606
+ category=CATEGORY_UNKNOWN,
607
+ reason="Empty command",
608
+ confidence="high",
609
+ )
610
+
611
+ semantics = analyze_command(command)
612
+ tokens = list(semantics.tokens)
613
+ if not tokens:
614
+ return MutativeResult(
615
+ is_mutative=False,
616
+ category=CATEGORY_UNKNOWN,
617
+ reason="No tokens after parsing",
618
+ confidence="high",
619
+ )
620
+
621
+ base_cmd = semantics.base_cmd
622
+ family = CLI_FAMILY_LOOKUP.get(base_cmd, "unknown")
623
+
624
+ # --- Step 1: Command alias fast-path ---
625
+ if base_cmd in COMMAND_ALIASES:
626
+ alias_category = COMMAND_ALIASES[base_cmd]
627
+ dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
628
+ return MutativeResult(
629
+ is_mutative=True,
630
+ category=alias_category,
631
+ verb=base_cmd,
632
+ dangerous_flags=dangerous_flags,
633
+ cli_family=family if family != "unknown" else "system",
634
+ confidence="high",
635
+ reason=f"Command alias '{base_cmd}' is {alias_category.lower()}",
636
+ )
637
+
638
+ # --- Step 2: Single-token command (no verb to extract) ---
639
+ if len(tokens) == 1:
640
+ return MutativeResult(
641
+ is_mutative=False,
642
+ category=CATEGORY_UNKNOWN,
643
+ verb=base_cmd,
644
+ cli_family=family,
645
+ confidence="low",
646
+ reason=f"Single-token command '{base_cmd}' with no verb",
647
+ )
648
+
649
+ # --- Step 3: Simulation flag override ---
650
+ if any(t.lower() in SIMULATION_FLAGS for t in tokens):
651
+ # Find the first non-flag token after base_cmd for the verb
652
+ verb, _ = _find_first_non_flag(semantics.semantic_head_tokens)
653
+ return MutativeResult(
654
+ is_mutative=False,
655
+ category=CATEGORY_SIMULATION,
656
+ verb=verb,
657
+ cli_family=family,
658
+ confidence="high",
659
+ reason=f"Simulation flag detected (command has --dry-run or equivalent)",
660
+ )
661
+
662
+ # --- Step 3.5: --help exemption (whitelist + positional boundary) ---
663
+ # A --help / -h invocation on a well-known CLI only prints usage text.
664
+ # Three simultaneous conditions are required so the exemption does not
665
+ # degrade safety on command-aliases or unknown tools:
666
+ # (a) CLI is in the whitelist (family OR base_cmd explicitly trusted)
667
+ # (b) a help flag is present in the PARSED flags (ignores strings
668
+ # that happen to contain "--help" inside a path or argument value)
669
+ # (c) the command invocation is simple (<=2 non-flag positional tokens):
670
+ # "gaia update --help" (1), "gaia approvals clean --help" (2) OK;
671
+ # "kubectl delete pod mypod --help" (3) stays T3 because the CLI
672
+ # may process the mutative args before honoring --help.
673
+ # Root cause: ghost pendings P-738355ab and P-0b06738b were created by
674
+ # `gaia update --help` and `gaia approvals clean --help` being
675
+ # classified as MUTATIVE; this exemption prevents recurrence.
676
+ if (
677
+ base_cmd in HELP_IDEMPOTENT_BASE_CMDS
678
+ or family in HELP_IDEMPOTENT_FAMILIES
679
+ ):
680
+ flag_set = set(semantics.flag_tokens)
681
+ # Count semantic non-flag tokens only. Shell redirect shorthands
682
+ # like "2>&1", ">file", "<file" appear as non-flag tokens in shlex
683
+ # output but carry no CLI semantic value -- filtering them keeps
684
+ # "gaia approvals clean --help 2>&1" at threshold 2 instead of 3.
685
+ semantic_non_flags = [
686
+ t for t in semantics.non_flag_tokens
687
+ if not _SHELL_REDIRECT_RE.match(t)
688
+ ]
689
+ if flag_set & HELP_FLAGS and len(semantic_non_flags) <= 2:
690
+ verb = (
691
+ semantic_non_flags[0]
692
+ if semantic_non_flags
693
+ else "help"
694
+ )
695
+ return MutativeResult(
696
+ is_mutative=False,
697
+ category=CATEGORY_READ_ONLY,
698
+ verb=verb,
699
+ cli_family=family,
700
+ confidence="high",
701
+ reason=(
702
+ f"--help on whitelisted CLI '{base_cmd}' "
703
+ f"with simple invocation (<=2 non-flag tokens)"
704
+ ),
705
+ )
706
+
707
+ # --- Step 3b: Inline code safety check (python3 -c, node -e, etc.) ---
708
+ # For runtime interpreters with inline code flags, scan the code string
709
+ # using the 3-layer approach instead of verb-matching tokens (which would
710
+ # false-positive on generic keywords like "import", "create", etc.).
711
+ cli_flags = _INLINE_CODE_MAP.get(base_cmd, frozenset())
712
+ if base_cmd in _INLINE_CODE_CLIS and cli_flags & set(semantics.flag_tokens):
713
+ return _check_inline_code(command, base_cmd, family)
714
+
715
+ # --- Step 3c: Heredoc safety check ---
716
+ # When a runtime interpreter is invoked with '-' (stdin) and the command
717
+ # contains a heredoc ('<<'), the heredoc body is script source --
718
+ # not shell subcommands. Route through inline code analysis.
719
+ # The length heuristic is suppressed: multi-line heredocs are normal
720
+ # and must not be flagged on size alone.
721
+ if (
722
+ base_cmd in _INLINE_CODE_CLIS
723
+ and "<<" in command
724
+ and semantics.non_flag_tokens
725
+ and semantics.non_flag_tokens[0] == "-"
726
+ ):
727
+ return _check_inline_code(command, base_cmd, family, skip_length_check=True)
728
+
729
+ # --- Step 3d: Git local-only subcommand guard ---
730
+ # When base_cmd is "git" and the subcommand is local-only (commit, stash,
731
+ # add, log, etc.), short-circuit to non-mutative. This prevents message
732
+ # body text after -m from leaking into the verb scanner and triggering
733
+ # false positives on words like "update", "create", "deploy".
734
+ # Dangerous flags (-D, -M, --force) are still checked so that
735
+ # "git branch -D feature" remains flagged.
736
+ if base_cmd == "git" and semantics.non_flag_tokens:
737
+ git_subcmd = semantics.non_flag_tokens[0]
738
+ if git_subcmd in GIT_LOCAL_SAFE_SUBCOMMANDS:
739
+ dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
740
+ if dangerous_flags:
741
+ return MutativeResult(
742
+ is_mutative=True,
743
+ category=CATEGORY_MUTATIVE,
744
+ verb=git_subcmd,
745
+ dangerous_flags=dangerous_flags,
746
+ cli_family=family,
747
+ confidence="high",
748
+ reason=f"Git local subcommand '{git_subcmd}' with dangerous flags {dangerous_flags}",
749
+ )
750
+ return MutativeResult(
751
+ is_mutative=False,
752
+ category=CATEGORY_SIMULATION if git_subcmd in SIMULATION_VERBS else CATEGORY_READ_ONLY if git_subcmd in READ_ONLY_VERBS else CATEGORY_UNKNOWN,
753
+ verb=git_subcmd,
754
+ cli_family=family,
755
+ confidence="high",
756
+ reason=f"Git local-only subcommand '{git_subcmd}' is safe",
757
+ )
758
+
759
+ # --- Step 4: Scan semantic non-flag tokens near the command head ---
760
+ # Priority order: SIMULATION > MUTATIVE > READ_ONLY > ALIASES
761
+ for semantic_index, token in enumerate(semantics.semantic_head_tokens[1:], start=1):
762
+ # Check compound read-only subcommands BEFORE hyphen-split.
763
+ # Without this, "merge-base" would be split to "merge" -> MUTATIVE.
764
+ if token in COMPOUND_READ_ONLY_SUBCOMMANDS:
765
+ return MutativeResult(
766
+ is_mutative=False,
767
+ category=CATEGORY_READ_ONLY,
768
+ verb=token,
769
+ cli_family=family,
770
+ confidence="high",
771
+ reason=f"Compound read-only subcommand '{token}'",
772
+ )
773
+
774
+ # Strip leading '+' macro prefix before verb lookup.
775
+ # Some CLIs (notably `gws`) expose convenience macros with a '+' prefix
776
+ # that wrap an underlying mutative API call:
777
+ # gws gmail +reply -> sends a reply (equivalent to messages send)
778
+ # gws gmail +send -> sends a new message
779
+ # gws gmail +search -> list/search wrapper (read-only)
780
+ # Without stripping '+', these tokens miss MUTATIVE_VERBS / READ_ONLY_VERBS
781
+ # lookups and fall through to "safe by elimination", bypassing T3 approval.
782
+ # Stripping here resolves the macro to its base verb so the taxonomy below
783
+ # classifies it correctly.
784
+ stripped_token = token.lstrip("+")
785
+
786
+ # Split hyphenated tokens: "delete-stack" -> check "delete"
787
+ candidate = stripped_token.split("-", 1)[0] if "-" in stripped_token else stripped_token
788
+
789
+ # Also check full token for exact matches (e.g., "force-delete")
790
+ full_lower = stripped_token
791
+
792
+ # Determine confidence from position
793
+ confidence = "high" if semantic_index <= 2 else "medium"
794
+
795
+ # Check verb taxonomy in priority order
796
+ if candidate in SIMULATION_VERBS or full_lower in SIMULATION_VERBS:
797
+ verb = candidate if candidate in SIMULATION_VERBS else full_lower
798
+ return MutativeResult(
799
+ is_mutative=False,
800
+ category=CATEGORY_SIMULATION,
801
+ verb=verb,
802
+ cli_family=family,
803
+ confidence=confidence,
804
+ reason=f"Simulation verb '{verb}'",
805
+ )
806
+
807
+ if candidate in MUTATIVE_VERBS or full_lower in MUTATIVE_VERBS:
808
+ verb = candidate if candidate in MUTATIVE_VERBS else full_lower
809
+
810
+ # Check verb+flag overrides: some verbs become READ_ONLY with
811
+ # specific flags (e.g., "git tag -l" is listing, not creating).
812
+ override_key = (family, verb)
813
+ if override_key in VERB_FLAG_READ_ONLY_OVERRIDES:
814
+ override_flags = VERB_FLAG_READ_ONLY_OVERRIDES[override_key]
815
+ if override_flags & frozenset(semantics.flag_tokens):
816
+ return MutativeResult(
817
+ is_mutative=False,
818
+ category=CATEGORY_READ_ONLY,
819
+ verb=verb,
820
+ cli_family=family,
821
+ confidence="high",
822
+ reason=f"Verb '{verb}' overridden to read-only by flag",
823
+ )
824
+
825
+ # Check unconditional tier exceptions: some (cli_family, verb)
826
+ # combos are safe despite the verb being in MUTATIVE_VERBS.
827
+ # Example: Gmail API "modify" only changes labels/flags.
828
+ exception_key = (family, verb)
829
+ if exception_key in CLI_VERB_TIER_EXCEPTIONS:
830
+ target_category = CLI_VERB_TIER_EXCEPTIONS[exception_key]
831
+ return MutativeResult(
832
+ is_mutative=False,
833
+ category=target_category,
834
+ verb=verb,
835
+ cli_family=family,
836
+ confidence="high",
837
+ reason=f"Verb '{verb}' for '{family}' CLI excepted to {target_category.lower()} by config",
838
+ )
839
+
840
+ dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
841
+ flag_detail = (
842
+ f" with dangerous flags {dangerous_flags}"
843
+ if dangerous_flags else ""
844
+ )
845
+ return MutativeResult(
846
+ is_mutative=True,
847
+ category=CATEGORY_MUTATIVE,
848
+ verb=verb,
849
+ dangerous_flags=dangerous_flags,
850
+ cli_family=family,
851
+ confidence=confidence,
852
+ reason=f"Mutative verb '{verb}'{flag_detail}",
853
+ )
854
+
855
+ # --- Secondary check: camelCase splitting ---
856
+ # "batchDelete" -> ["batch", "delete"] -> "delete" is in MUTATIVE_VERBS
857
+ # Use the raw (original-case) token because semantic_head_tokens is
858
+ # lowercased, which destroys the camelCase word boundaries that
859
+ # split_camel_case relies on (regex: [a-z][A-Z]).
860
+ raw_token = semantics.semantic_head_tokens_raw[semantic_index] if semantic_index < len(semantics.semantic_head_tokens_raw) else token
861
+ camel_parts = split_camel_case(raw_token)
862
+ if len(camel_parts) > 1:
863
+ for part in camel_parts:
864
+ if part in MUTATIVE_VERBS:
865
+ override_key = (family, part)
866
+ if override_key in VERB_FLAG_READ_ONLY_OVERRIDES:
867
+ override_flags = VERB_FLAG_READ_ONLY_OVERRIDES[override_key]
868
+ if override_flags & frozenset(semantics.flag_tokens):
869
+ return MutativeResult(
870
+ is_mutative=False,
871
+ category=CATEGORY_READ_ONLY,
872
+ verb=part,
873
+ cli_family=family,
874
+ confidence="high",
875
+ reason=f"CamelCase verb '{part}' (from '{raw_token}') overridden to read-only by flag",
876
+ )
877
+ if (family, part) in CLI_VERB_TIER_EXCEPTIONS:
878
+ target_category = CLI_VERB_TIER_EXCEPTIONS[(family, part)]
879
+ return MutativeResult(
880
+ is_mutative=False,
881
+ category=target_category,
882
+ verb=part,
883
+ cli_family=family,
884
+ confidence="high",
885
+ reason=f"CamelCase verb '{part}' (from '{raw_token}') excepted to {target_category.lower()} by config",
886
+ )
887
+ dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
888
+ flag_detail = (
889
+ f" with dangerous flags {dangerous_flags}"
890
+ if dangerous_flags else ""
891
+ )
892
+ return MutativeResult(
893
+ is_mutative=True,
894
+ category=CATEGORY_MUTATIVE,
895
+ verb=part,
896
+ dangerous_flags=dangerous_flags,
897
+ cli_family=family,
898
+ confidence=confidence,
899
+ reason=f"CamelCase verb '{part}' (from '{raw_token}'){flag_detail}",
900
+ )
901
+
902
+ if candidate in READ_ONLY_VERBS or full_lower in READ_ONLY_VERBS:
903
+ verb = candidate if candidate in READ_ONLY_VERBS else full_lower
904
+ return MutativeResult(
905
+ is_mutative=False,
906
+ category=CATEGORY_READ_ONLY,
907
+ verb=verb,
908
+ cli_family=family,
909
+ confidence=confidence,
910
+ reason=f"Read-only verb '{verb}'",
911
+ )
912
+
913
+ # Check command aliases as verb (e.g., "docker rm" -> rm is alias)
914
+ if candidate in COMMAND_ALIASES:
915
+ alias_cat = COMMAND_ALIASES[candidate]
916
+ dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
917
+ return MutativeResult(
918
+ is_mutative=True,
919
+ category=alias_cat,
920
+ verb=candidate,
921
+ dangerous_flags=dangerous_flags,
922
+ cli_family=family,
923
+ confidence=confidence,
924
+ reason=f"Verb alias '{candidate}' is {alias_cat.lower()}",
925
+ )
926
+
927
+ # --- Step 4b: API subcommand with no explicit mutative HTTP method ---
928
+ # CLIs like `gh api` and `glab api` default to GET when no -X flag is
929
+ # specified. If the semantic scan found no verb and the subcommand is
930
+ # "api", treat the command as read-only.
931
+ if (
932
+ not any(
933
+ t in MUTATIVE_VERBS
934
+ for t in semantics.semantic_head_tokens[1:]
935
+ )
936
+ and len(semantics.semantic_head_tokens) > 1
937
+ and semantics.semantic_head_tokens[1] == "api"
938
+ ):
939
+ return MutativeResult(
940
+ is_mutative=False,
941
+ category=CATEGORY_READ_ONLY,
942
+ verb="api",
943
+ cli_family=family,
944
+ confidence="high",
945
+ reason="API call with implicit GET method",
946
+ )
947
+
948
+ # --- Step 5: Scan for dangerous flags (no verb found) ---
949
+ dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
950
+ if dangerous_flags:
951
+ # Find first non-flag token as the "verb" for reporting
952
+ verb, _ = _find_first_non_flag(semantics.semantic_head_tokens)
953
+ return MutativeResult(
954
+ is_mutative=True,
955
+ category=CATEGORY_UNKNOWN,
956
+ verb=verb,
957
+ dangerous_flags=dangerous_flags,
958
+ cli_family=family,
959
+ confidence="low",
960
+ reason=f"Unknown verb '{verb}' with dangerous flags {dangerous_flags}",
961
+ )
962
+
963
+ # --- Step 6: No match -- not mutative (safe by elimination) ---
964
+ verb, _ = _find_first_non_flag(semantics.semantic_head_tokens)
965
+ return MutativeResult(
966
+ is_mutative=False,
967
+ category=CATEGORY_UNKNOWN,
968
+ verb=verb,
969
+ cli_family=family,
970
+ confidence="low",
971
+ reason=f"Unknown verb '{verb}' with no dangerous flags",
972
+ )
973
+
974
+
975
+ # ============================================================================
976
+ # Helpers
977
+ # ============================================================================
978
+
979
+ def _check_inline_code(command: str, base_cmd: str, family: str, skip_length_check: bool = False) -> MutativeResult:
980
+ """Check inline code for dangerous patterns using a 3-layer approach.
981
+
982
+ Layer 1: Extract string literals from inline code and check them against
983
+ blocked_commands (catches embedded shell commands like 'rm -rf /').
984
+ Layer 2: Scan for universal dangerous API keywords (language-agnostic).
985
+ Layer 3: Heuristic safety classification (length, sensitive paths, encoding).
986
+
987
+ Args:
988
+ command: Full raw command string.
989
+ base_cmd: The interpreter (e.g., "python3", "node", "ruby").
990
+ family: CLI family hint.
991
+
992
+ Returns:
993
+ MutativeResult -- MUTATIVE if any layer triggers, else safe.
994
+ """
995
+ # ---- Layer 1: Extract string literals → check against blocked_commands ----
996
+ if _is_blocked_command is not None:
997
+ embedded_strings = _extract_embedded_shell_commands(command)
998
+ for literal in embedded_strings:
999
+ blocked = _is_blocked_command(literal)
1000
+ if blocked.is_blocked:
1001
+ return MutativeResult(
1002
+ is_mutative=True,
1003
+ category=CATEGORY_MUTATIVE,
1004
+ verb="embedded-blocked-cmd",
1005
+ cli_family=family,
1006
+ confidence="high",
1007
+ reason=(
1008
+ f"Inline code contains blocked shell command in "
1009
+ f"string literal: {blocked.category}"
1010
+ ),
1011
+ )
1012
+
1013
+ # ---- Layer 2: Universal dangerous API keyword patterns ----
1014
+ for pattern, label, category in _UNIVERSAL_DANGEROUS_PATTERNS:
1015
+ if pattern.search(command):
1016
+ return MutativeResult(
1017
+ is_mutative=True,
1018
+ category=CATEGORY_MUTATIVE,
1019
+ verb=label,
1020
+ cli_family=family,
1021
+ confidence="medium",
1022
+ reason=f"Inline code contains dangerous pattern: {label} ({category})",
1023
+ )
1024
+
1025
+ # ---- Layer 3: Heuristic safety classification ----
1026
+ # 3a: Check for suspicious indicators (sensitive paths, encoding, IPs)
1027
+ for pattern, label in _SUSPICIOUS_HEURISTICS:
1028
+ if pattern.search(command):
1029
+ return MutativeResult(
1030
+ is_mutative=True,
1031
+ category=CATEGORY_MUTATIVE,
1032
+ verb=f"heuristic-{label}",
1033
+ cli_family=family,
1034
+ confidence="low",
1035
+ reason=f"Inline code flagged by heuristic: {label}",
1036
+ )
1037
+
1038
+ # 3b: Unusually long inline code is suspicious
1039
+ # Extract the code portion after the inline flag for length check.
1040
+ # Use a rough extraction: everything after the first inline flag.
1041
+ code_portion = command
1042
+ cli_flag_tokens = _INLINE_CODE_MAP.get(base_cmd, frozenset())
1043
+ for flag in cli_flag_tokens:
1044
+ idx = command.find(f" {flag} ")
1045
+ if idx != -1:
1046
+ code_portion = command[idx + len(flag) + 2:]
1047
+ break
1048
+
1049
+ if not skip_length_check and len(code_portion) > MAX_NORMAL_INLINE_LENGTH:
1050
+ return MutativeResult(
1051
+ is_mutative=True,
1052
+ category=CATEGORY_MUTATIVE,
1053
+ verb="heuristic-long-code",
1054
+ cli_family=family,
1055
+ confidence="low",
1056
+ reason=(
1057
+ f"Inline code is unusually long ({len(code_portion)} chars > "
1058
+ f"{MAX_NORMAL_INLINE_LENGTH} limit)"
1059
+ ),
1060
+ )
1061
+
1062
+ # ---- No layers triggered -- safe inline code ----
1063
+ return MutativeResult(
1064
+ is_mutative=False,
1065
+ category=CATEGORY_READ_ONLY,
1066
+ verb="inline-code",
1067
+ cli_family=family,
1068
+ confidence="medium",
1069
+ reason=f"Inline code ({base_cmd}) with no dangerous patterns",
1070
+ )
1071
+
1072
+
1073
+ def _find_first_non_flag(tokens: Union[List[str], tuple]) -> tuple:
1074
+ """Find the first semantic token after tokens[0].
1075
+
1076
+ Returns:
1077
+ (verb, position) tuple. ("", -1) if no non-flag token found.
1078
+ """
1079
+ for i in range(1, len(tokens)):
1080
+ if tokens[i]:
1081
+ return tokens[i], i
1082
+ return "", -1
1083
+
1084
+
1085
+ # ============================================================================
1086
+ # Hook Response Builder
1087
+ # ============================================================================
1088
+
1089
+ def build_t3_block_response(
1090
+ command: str,
1091
+ danger: MutativeResult,
1092
+ nonce: str = "",
1093
+ ) -> dict:
1094
+ """Build an internal block response dict for T3 commands.
1095
+
1096
+ Returns an internal dict consumed by bash_validator, which wraps the
1097
+ 'message' field into a hookSpecificOutput with permissionDecision: "deny".
1098
+ The 'decision' key is internal only and never sent to Claude Code.
1099
+
1100
+ Args:
1101
+ command: The original shell command.
1102
+ danger: MutativeResult from detect_mutative_command.
1103
+ nonce: Cryptographic nonce for this pending approval. When provided,
1104
+ the block message includes the approval code that the agent must
1105
+ present to the user.
1106
+
1107
+ Returns:
1108
+ Dict with 'decision' (internal) and 'message' (forwarded to agent) keys.
1109
+ """
1110
+ flag_warning = ""
1111
+ if danger.dangerous_flags:
1112
+ flag_warning = (
1113
+ f"\nDangerous flags detected: {', '.join(danger.dangerous_flags)}"
1114
+ )
1115
+
1116
+ message = (
1117
+ f"[T3_APPROVAL_REQUIRED] {danger.category} operation detected.\n"
1118
+ f"Command: {command}\n"
1119
+ f"Verb: '{danger.verb}' (CLI family: {danger.cli_family})\n"
1120
+ f"Confidence: {danger.confidence}\n"
1121
+ f"Reason: {danger.reason}{flag_warning}\n"
1122
+ f"\n"
1123
+ f"{build_t3_approval_instructions(nonce)}"
1124
+ )
1125
+
1126
+ return {
1127
+ "decision": "block",
1128
+ "message": message,
1129
+ }
1130
+
1131
+