@jaguilar87/gaia-ops 4.4.0 → 4.7.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 (371) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +12 -3
  3. package/ARCHITECTURE.md +9 -8
  4. package/CHANGELOG.md +34 -0
  5. package/README.md +43 -11
  6. package/agents/terraform-architect.md +1 -1
  7. package/bin/README.md +2 -2
  8. package/bin/gaia-doctor.js +18 -5
  9. package/bin/gaia-history.js +0 -1
  10. package/bin/gaia-metrics.js +2 -2
  11. package/bin/gaia-scan.py +23 -1
  12. package/bin/gaia-update.js +346 -54
  13. package/bin/pre-publish-validate.js +33 -10
  14. package/commands/gaia.md +37 -0
  15. package/config/README.md +3 -9
  16. package/config/context-contracts.json +47 -15
  17. package/config/surface-routing.json +9 -1
  18. package/dist/gaia-ops/.claude-plugin/plugin.json +22 -0
  19. package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
  20. package/dist/gaia-ops/agents/devops-developer.md +57 -0
  21. package/dist/gaia-ops/agents/gaia-system.md +58 -0
  22. package/dist/gaia-ops/agents/gitops-operator.md +60 -0
  23. package/dist/gaia-ops/agents/speckit-planner.md +71 -0
  24. package/dist/gaia-ops/agents/terraform-architect.md +60 -0
  25. package/dist/gaia-ops/commands/gaia.md +37 -0
  26. package/dist/gaia-ops/config/README.md +58 -0
  27. package/dist/gaia-ops/config/cloud/aws.json +140 -0
  28. package/dist/gaia-ops/config/cloud/gcp.json +145 -0
  29. package/dist/gaia-ops/config/context-contracts.json +131 -0
  30. package/dist/gaia-ops/config/git_standards.json +72 -0
  31. package/dist/gaia-ops/config/surface-routing.json +197 -0
  32. package/dist/gaia-ops/config/universal-rules.json +10 -0
  33. package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
  34. package/dist/gaia-ops/hooks/adapters/base.py +219 -0
  35. package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
  36. package/dist/gaia-ops/hooks/adapters/claude_code.py +1477 -0
  37. package/dist/gaia-ops/hooks/adapters/types.py +194 -0
  38. package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
  39. package/dist/gaia-ops/hooks/hooks.json +126 -0
  40. package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
  41. package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
  42. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
  43. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
  44. package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +124 -0
  45. package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
  46. package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
  47. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
  48. package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
  49. package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
  50. package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
  51. package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
  52. package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +576 -0
  53. package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
  54. package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
  55. package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
  56. package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +215 -0
  57. package/dist/gaia-ops/hooks/modules/context/context_cache.py +129 -0
  58. package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
  59. package/dist/gaia-ops/hooks/modules/context/context_injector.py +427 -0
  60. package/dist/gaia-ops/hooks/modules/context/context_writer.py +518 -0
  61. package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
  62. package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
  63. package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
  64. package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
  65. package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
  66. package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +558 -0
  67. package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
  68. package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
  69. package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
  70. package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
  71. package/dist/gaia-ops/hooks/modules/identity/__init__.py +0 -0
  72. package/dist/gaia-ops/hooks/modules/identity/identity_provider.py +21 -0
  73. package/dist/gaia-ops/hooks/modules/identity/ops_identity.py +34 -0
  74. package/dist/gaia-ops/hooks/modules/identity/security_identity.py +10 -0
  75. package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
  76. package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +227 -0
  77. package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
  78. package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +128 -0
  79. package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
  80. package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
  81. package/dist/gaia-ops/hooks/modules/security/__init__.py +89 -0
  82. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
  83. package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
  84. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +912 -0
  85. package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
  86. package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +153 -0
  87. package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +584 -0
  88. package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +86 -0
  89. package/dist/gaia-ops/hooks/modules/security/command_semantics.py +130 -0
  90. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
  91. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +850 -0
  92. package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
  93. package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
  94. package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
  95. package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
  96. package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +158 -0
  97. package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
  98. package/dist/gaia-ops/hooks/modules/tools/__init__.py +25 -0
  99. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +708 -0
  100. package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +181 -0
  101. package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
  102. package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
  103. package/dist/gaia-ops/hooks/modules/tools/task_validator.py +283 -0
  104. package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
  105. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
  106. package/dist/gaia-ops/hooks/post_compact.py +43 -0
  107. package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
  108. package/dist/gaia-ops/hooks/pre_tool_use.py +383 -0
  109. package/dist/gaia-ops/hooks/session_start.py +69 -0
  110. package/dist/gaia-ops/hooks/stop_hook.py +69 -0
  111. package/dist/gaia-ops/hooks/subagent_start.py +71 -0
  112. package/dist/gaia-ops/hooks/subagent_stop.py +288 -0
  113. package/dist/gaia-ops/hooks/task_completed.py +70 -0
  114. package/dist/gaia-ops/hooks/user_prompt_submit.py +177 -0
  115. package/dist/gaia-ops/settings.json +72 -0
  116. package/dist/gaia-ops/skills/README.md +109 -0
  117. package/dist/gaia-ops/skills/agent-protocol/SKILL.md +105 -0
  118. package/dist/gaia-ops/skills/agent-protocol/examples.md +170 -0
  119. package/dist/gaia-ops/skills/agent-response/SKILL.md +53 -0
  120. package/dist/gaia-ops/skills/approval/SKILL.md +85 -0
  121. package/dist/gaia-ops/skills/approval/examples.md +140 -0
  122. package/dist/gaia-ops/skills/approval/reference.md +57 -0
  123. package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
  124. package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
  125. package/dist/gaia-ops/skills/context-updater/SKILL.md +76 -0
  126. package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
  127. package/dist/gaia-ops/skills/developer-patterns/SKILL.md +93 -0
  128. package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
  129. package/dist/gaia-ops/skills/execution/SKILL.md +66 -0
  130. package/dist/gaia-ops/skills/fast-queries/SKILL.md +47 -0
  131. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +92 -0
  132. package/dist/gaia-ops/skills/gaia-patterns/reference.md +22 -0
  133. package/dist/gaia-ops/skills/git-conventions/SKILL.md +48 -0
  134. package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +73 -0
  135. package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
  136. package/dist/gaia-ops/skills/investigation/SKILL.md +77 -0
  137. package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +64 -0
  138. package/dist/gaia-ops/skills/reference.md +134 -0
  139. package/dist/gaia-ops/skills/security-tiers/SKILL.md +61 -0
  140. package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
  141. package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
  142. package/dist/gaia-ops/skills/skill-creation/SKILL.md +119 -0
  143. package/dist/gaia-ops/skills/specification/SKILL.md +186 -0
  144. package/dist/gaia-ops/skills/speckit-workflow/SKILL.md +165 -0
  145. package/dist/gaia-ops/skills/speckit-workflow/reference.md +117 -0
  146. package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +63 -0
  147. package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
  148. package/dist/gaia-ops/speckit/README.md +516 -0
  149. package/dist/gaia-ops/speckit/scripts/.gitkeep +0 -0
  150. package/dist/gaia-ops/speckit/templates/adr-template.md +118 -0
  151. package/dist/gaia-ops/speckit/templates/agent-file-template.md +23 -0
  152. package/dist/gaia-ops/speckit/templates/plan-template.md +227 -0
  153. package/dist/gaia-ops/speckit/templates/spec-template.md +140 -0
  154. package/dist/gaia-ops/speckit/templates/tasks-template.md +257 -0
  155. package/dist/gaia-ops/tools/context/README.md +132 -0
  156. package/dist/gaia-ops/tools/context/__init__.py +42 -0
  157. package/dist/gaia-ops/tools/context/_paths.py +20 -0
  158. package/dist/gaia-ops/tools/context/context_provider.py +476 -0
  159. package/dist/gaia-ops/tools/context/context_section_reader.py +330 -0
  160. package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
  161. package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
  162. package/dist/gaia-ops/tools/context/surface_router.py +278 -0
  163. package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
  164. package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
  165. package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  166. package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  167. package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  168. package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  169. package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
  170. package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  171. package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
  172. package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
  173. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
  174. package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
  175. package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
  176. package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
  177. package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +262 -0
  178. package/dist/gaia-ops/tools/memory/README.md +0 -0
  179. package/dist/gaia-ops/tools/memory/__init__.py +20 -0
  180. package/dist/gaia-ops/tools/memory/episodic.py +1196 -0
  181. package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
  182. package/dist/gaia-ops/tools/review/__init__.py +1 -0
  183. package/dist/gaia-ops/tools/review/review_engine.py +157 -0
  184. package/dist/gaia-ops/tools/scan/__init__.py +35 -0
  185. package/dist/gaia-ops/tools/scan/config.py +247 -0
  186. package/dist/gaia-ops/tools/scan/merge.py +212 -0
  187. package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
  188. package/dist/gaia-ops/tools/scan/registry.py +127 -0
  189. package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
  190. package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
  191. package/dist/gaia-ops/tools/scan/scanners/environment.py +324 -0
  192. package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
  193. package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
  194. package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
  195. package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
  196. package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
  197. package/dist/gaia-ops/tools/scan/setup.py +753 -0
  198. package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
  199. package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
  200. package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
  201. package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
  202. package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
  203. package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
  204. package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
  205. package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
  206. package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
  207. package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
  208. package/dist/gaia-ops/tools/scan/ui.py +624 -0
  209. package/dist/gaia-ops/tools/scan/verify.py +266 -0
  210. package/dist/gaia-ops/tools/scan/walk.py +118 -0
  211. package/dist/gaia-ops/tools/scan/workspace.py +85 -0
  212. package/dist/gaia-ops/tools/validation/README.md +244 -0
  213. package/dist/gaia-ops/tools/validation/__init__.py +17 -0
  214. package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
  215. package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
  216. package/dist/gaia-security/.claude-plugin/plugin.json +22 -0
  217. package/dist/gaia-security/config/universal-rules.json +10 -0
  218. package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
  219. package/dist/gaia-security/hooks/adapters/base.py +219 -0
  220. package/dist/gaia-security/hooks/adapters/channel.py +17 -0
  221. package/dist/gaia-security/hooks/adapters/claude_code.py +1477 -0
  222. package/dist/gaia-security/hooks/adapters/types.py +194 -0
  223. package/dist/gaia-security/hooks/adapters/utils.py +25 -0
  224. package/dist/gaia-security/hooks/hooks.json +57 -0
  225. package/dist/gaia-security/hooks/modules/__init__.py +15 -0
  226. package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
  227. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
  228. package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
  229. package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +124 -0
  230. package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
  231. package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
  232. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
  233. package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
  234. package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
  235. package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
  236. package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
  237. package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +576 -0
  238. package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
  239. package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
  240. package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
  241. package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +215 -0
  242. package/dist/gaia-security/hooks/modules/context/context_cache.py +129 -0
  243. package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
  244. package/dist/gaia-security/hooks/modules/context/context_injector.py +427 -0
  245. package/dist/gaia-security/hooks/modules/context/context_writer.py +518 -0
  246. package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
  247. package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
  248. package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
  249. package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
  250. package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
  251. package/dist/gaia-security/hooks/modules/core/plugin_setup.py +558 -0
  252. package/dist/gaia-security/hooks/modules/core/state.py +179 -0
  253. package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
  254. package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
  255. package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
  256. package/dist/gaia-security/hooks/modules/identity/__init__.py +0 -0
  257. package/dist/gaia-security/hooks/modules/identity/identity_provider.py +21 -0
  258. package/dist/gaia-security/hooks/modules/identity/ops_identity.py +34 -0
  259. package/dist/gaia-security/hooks/modules/identity/security_identity.py +10 -0
  260. package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
  261. package/dist/gaia-security/hooks/modules/memory/episode_writer.py +227 -0
  262. package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
  263. package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +128 -0
  264. package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
  265. package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
  266. package/dist/gaia-security/hooks/modules/security/__init__.py +89 -0
  267. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
  268. package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
  269. package/dist/gaia-security/hooks/modules/security/approval_grants.py +912 -0
  270. package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
  271. package/dist/gaia-security/hooks/modules/security/approval_scopes.py +153 -0
  272. package/dist/gaia-security/hooks/modules/security/blocked_commands.py +584 -0
  273. package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +86 -0
  274. package/dist/gaia-security/hooks/modules/security/command_semantics.py +130 -0
  275. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
  276. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +850 -0
  277. package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
  278. package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
  279. package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
  280. package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
  281. package/dist/gaia-security/hooks/modules/session/session_event_injector.py +158 -0
  282. package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
  283. package/dist/gaia-security/hooks/modules/tools/__init__.py +25 -0
  284. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +708 -0
  285. package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +181 -0
  286. package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
  287. package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
  288. package/dist/gaia-security/hooks/modules/tools/task_validator.py +283 -0
  289. package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
  290. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
  291. package/dist/gaia-security/hooks/post_tool_use.py +54 -0
  292. package/dist/gaia-security/hooks/pre_tool_use.py +383 -0
  293. package/dist/gaia-security/hooks/session_start.py +69 -0
  294. package/dist/gaia-security/hooks/stop_hook.py +69 -0
  295. package/dist/gaia-security/hooks/user_prompt_submit.py +177 -0
  296. package/dist/gaia-security/settings.json +58 -0
  297. package/git-hooks/commit-msg +41 -0
  298. package/hooks/README.md +8 -6
  299. package/hooks/adapters/channel.py +0 -25
  300. package/hooks/adapters/claude_code.py +364 -125
  301. package/hooks/elicitation_result.py +132 -0
  302. package/hooks/hooks.json +10 -1
  303. package/hooks/modules/README.md +3 -2
  304. package/hooks/modules/agents/contract_validator.py +3 -51
  305. package/hooks/modules/agents/response_contract.py +4 -8
  306. package/hooks/modules/agents/transcript_reader.py +4 -5
  307. package/hooks/modules/audit/__init__.py +4 -6
  308. package/hooks/modules/audit/event_detector.py +0 -2
  309. package/hooks/modules/audit/metrics.py +108 -187
  310. package/hooks/modules/audit/workflow_auditor.py +0 -4
  311. package/hooks/modules/audit/workflow_recorder.py +0 -5
  312. package/hooks/modules/context/compact_context_builder.py +1 -0
  313. package/hooks/modules/context/context_cache.py +129 -0
  314. package/hooks/modules/context/context_injector.py +18 -40
  315. package/hooks/modules/context/context_writer.py +1 -25
  316. package/hooks/modules/context/contracts_loader.py +7 -10
  317. package/hooks/modules/core/hook_entry.py +1 -0
  318. package/hooks/modules/core/paths.py +12 -13
  319. package/hooks/modules/core/plugin_mode.py +74 -4
  320. package/hooks/modules/core/plugin_setup.py +395 -23
  321. package/hooks/modules/events/__init__.py +1 -0
  322. package/hooks/modules/events/event_writer.py +210 -0
  323. package/hooks/modules/identity/ops_identity.py +18 -27
  324. package/hooks/modules/memory/episode_writer.py +1 -6
  325. package/hooks/modules/orchestrator/__init__.py +1 -0
  326. package/hooks/modules/orchestrator/delegate_mode.py +128 -0
  327. package/hooks/modules/security/__init__.py +2 -4
  328. package/hooks/modules/security/approval_constants.py +5 -1
  329. package/hooks/modules/security/approval_grants.py +189 -6
  330. package/hooks/modules/security/approval_messages.py +9 -21
  331. package/hooks/modules/security/blocked_commands.py +98 -34
  332. package/hooks/modules/security/command_semantics.py +0 -4
  333. package/hooks/modules/security/gitops_validator.py +1 -11
  334. package/hooks/modules/security/mutative_verbs.py +179 -38
  335. package/hooks/modules/security/tiers.py +1 -19
  336. package/hooks/modules/session/session_event_injector.py +1 -25
  337. package/hooks/modules/tools/bash_validator.py +310 -94
  338. package/hooks/modules/tools/shell_parser.py +0 -1
  339. package/hooks/modules/tools/task_validator.py +9 -29
  340. package/hooks/post_tool_use.py +0 -72
  341. package/hooks/pre_tool_use.py +42 -102
  342. package/hooks/session_start.py +4 -2
  343. package/hooks/subagent_start.py +6 -2
  344. package/hooks/subagent_stop.py +1 -13
  345. package/hooks/user_prompt_submit.py +119 -37
  346. package/index.js +1 -1
  347. package/package.json +5 -3
  348. package/skills/README.md +3 -5
  349. package/skills/agent-protocol/SKILL.md +17 -16
  350. package/skills/agent-protocol/examples.md +6 -6
  351. package/skills/agent-response/SKILL.md +11 -14
  352. package/skills/approval/SKILL.md +28 -13
  353. package/skills/approval/reference.md +2 -2
  354. package/skills/execution/SKILL.md +1 -1
  355. package/skills/gaia-patterns/SKILL.md +2 -3
  356. package/skills/orchestrator-approval/SKILL.md +22 -50
  357. package/skills/security-tiers/SKILL.md +1 -1
  358. package/templates/README.md +9 -9
  359. package/templates/managed-settings.template.json +43 -0
  360. package/tools/gaia_simulator/runner.py +34 -1
  361. package/tools/scan/orchestrator.py +13 -0
  362. package/tools/scan/scanners/base.py +8 -0
  363. package/tools/scan/scanners/git.py +78 -0
  364. package/tools/scan/scanners/infrastructure.py +65 -0
  365. package/tools/scan/scanners/stack.py +110 -0
  366. package/tools/scan/setup.py +120 -13
  367. package/tools/scan/workspace.py +85 -0
  368. package/config/context-contracts.aws.json +0 -42
  369. package/config/context-contracts.gcp.json +0 -39
  370. package/skills/project-dispatch/SKILL.md +0 -34
  371. package/templates/settings.template.json +0 -226
@@ -0,0 +1,850 @@
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
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
+ "deploy", "install", "upgrade", "downgrade", "publish", "release", "promote",
90
+ # Scaling
91
+ "scale", "resize", "autoscale",
92
+ # Lifecycle
93
+ "start", "restart", "reboot", "reload", "refresh", "resume",
94
+ "uncordon", "unsuspend", "enable", "disable", "suspend", "pause",
95
+ "stop", "shutdown", "halt", "abort",
96
+ # Movement / transfer
97
+ "move", "rename", "copy", "sync",
98
+ "import", "export", "migrate", "transfer",
99
+ # Attachment
100
+ # NOTE: "link" removed -- false positive in shell variable names (e.g., "for link in ...").
101
+ # The `ln` command is already covered as a COMMAND_ALIAS.
102
+ "attach", "bind", "connect", "mount",
103
+ # Execution
104
+ # NOTE: "run" removed -- safe by elimination (e.g., docker run is common dev workflow)
105
+ "exec", "execute", "invoke", "trigger", "send",
106
+ # Git operations
107
+ # NOTE: "stash" removed -- safe by elimination (local-only operation)
108
+ "commit", "push", "merge", "rebase", "cherry-pick",
109
+ "revert", "rollback",
110
+ # Access control
111
+ "grant", "assign", "revoke",
112
+ # Reconciliation
113
+ "reconcile", "rsync",
114
+ # Deletion / removal (approvable via nonce -- blocked_commands.py catches
115
+ # the truly destructive patterns like "delete namespace", "delete-vpc", etc.)
116
+ "delete", "destroy", "remove", "drop", "purge", "wipe", "clean",
117
+ "truncate", "kill", "terminate", "uninstall", "unpublish",
118
+ "drain", "evict", "cordon", "deregister", "detach",
119
+ "disconnect", "unbind", "reset", "force-delete", "force-remove", "erase",
120
+ # Collaboration (GitHub/GitLab CLI)
121
+ "comment", "label", "annotate", "approve", "close", "reopen", "tag",
122
+ # Helm-specific
123
+ "uninstall",
124
+ # HTTP methods (e.g., glab api -X POST, gh api -X DELETE)
125
+ "post", "put", "patch",
126
+ })
127
+
128
+ SIMULATION_VERBS: FrozenSet[str] = frozenset({
129
+ "plan", "diff", "preview", "template", "render", "simulate",
130
+ "test", "check", "verify", "lint", "validate", "fmt", "format", "audit",
131
+ })
132
+
133
+ READ_ONLY_VERBS: FrozenSet[str] = frozenset({
134
+ "get", "list", "describe", "show", "read", "view", "inspect",
135
+ "info", "status", "log", "logs", "tail", "head",
136
+ "search", "find", "query", "scan", "fetch", "download",
137
+ "version", "help", "whoami", "which", "explain",
138
+ "top", "stat", "history", "blame", "tree", "shortlog", "reflog",
139
+ "env", "auth", "config", "cluster-info", "api-resources", "ls",
140
+ # Compound subcommands that look mutative after hyphen-split but are read-only
141
+ "merge-base",
142
+ })
143
+
144
+
145
+ # ============================================================================
146
+ # Compound Read-Only Subcommands
147
+ # ============================================================================
148
+ # Full subcommand tokens that must be matched BEFORE the hyphen-split logic.
149
+ # Without this, "merge-base" would be split to "merge" and flagged as MUTATIVE.
150
+
151
+ COMPOUND_READ_ONLY_SUBCOMMANDS: FrozenSet[str] = frozenset({
152
+ "merge-base",
153
+ })
154
+
155
+
156
+ # ============================================================================
157
+ # Verb+Flag Overrides (mutative verb downgraded to READ_ONLY by a flag)
158
+ # ============================================================================
159
+ # Map of (cli_family, verb) -> frozenset of flag tokens that override to READ_ONLY.
160
+ # Checked AFTER a mutative verb is found but BEFORE returning the MUTATIVE result.
161
+
162
+ VERB_FLAG_READ_ONLY_OVERRIDES: Dict[Tuple[str, str], FrozenSet[str]] = {
163
+ # "git tag -l" / "git tag --list" is listing, not creating/deleting
164
+ ("git", "tag"): frozenset({"-l", "--list"}),
165
+ }
166
+
167
+
168
+ # ============================================================================
169
+ # Inline Code Detection — Language-Agnostic 3-Layer Approach
170
+ # ============================================================================
171
+ # When the base command is a runtime interpreter with an inline code flag
172
+ # (e.g., python3 -c, node -e, ruby -e, perl -e), scan the code string
173
+ # using three layers instead of verb-matching tokens:
174
+ # Layer 1: Extract string literals → check against blocked_commands
175
+ # Layer 2: Universal dangerous API keyword patterns
176
+ # Layer 3: Heuristic safety classification (length, paths, encoding)
177
+ import re as _re
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # CLI → inline-code flag mapping (Step 1a)
181
+ # ---------------------------------------------------------------------------
182
+ _INLINE_CODE_MAP: Dict[str, FrozenSet[str]] = {
183
+ "python": frozenset({"-c"}),
184
+ "python3": frozenset({"-c"}),
185
+ "python3.10": frozenset({"-c"}),
186
+ "python3.11": frozenset({"-c"}),
187
+ "python3.12": frozenset({"-c"}),
188
+ "python3.13": frozenset({"-c"}),
189
+ "node": frozenset({"-e", "--eval"}),
190
+ "ruby": frozenset({"-e"}),
191
+ "perl": frozenset({"-e", "-E"}),
192
+ "php": frozenset({"-r"}),
193
+ "lua": frozenset({"-e"}),
194
+ "rscript": frozenset({"-e"}),
195
+ }
196
+ _INLINE_CODE_CLIS: FrozenSet[str] = frozenset(_INLINE_CODE_MAP.keys())
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # Layer 1: Shell command extraction from string literals
200
+ # ---------------------------------------------------------------------------
201
+ _STRING_LITERAL_RE = _re.compile(r"""(?:['"])((?:[^'"\\\n]|\\.){3,})(?:['"])""")
202
+
203
+
204
+ def _extract_embedded_shell_commands(code: str) -> List[str]:
205
+ """Extract string literals from inline code that may contain shell commands."""
206
+ return [m.group(1) for m in _STRING_LITERAL_RE.finditer(code)]
207
+
208
+
209
+ # ---------------------------------------------------------------------------
210
+ # Layer 2: Universal dangerous API keyword patterns (category-based)
211
+ # ---------------------------------------------------------------------------
212
+ _UNIVERSAL_DANGEROUS_PATTERNS: Tuple[Tuple[_re.Pattern, str, str], ...] = (
213
+ # Category: Process Execution
214
+ (_re.compile(r"\b(child_process|subprocess)\b"), "process-module", "PROCESS_EXECUTION"),
215
+ (_re.compile(r"\b(execSync|execFile|execFileSync)\s*\("), "exec-sync", "PROCESS_EXECUTION"),
216
+ (_re.compile(r"\bos\.(system|popen|exec[lv]?[pe]?)\s*\("), "os-exec", "PROCESS_EXECUTION"),
217
+ (_re.compile(r"\b(system|exec)\s*\("), "system-call", "PROCESS_EXECUTION"),
218
+ (_re.compile(r"\bspawn(Sync)?\s*\("), "spawn-call", "PROCESS_EXECUTION"),
219
+ (_re.compile(r"\bPopen\s*\("), "popen-call", "PROCESS_EXECUTION"),
220
+ (_re.compile(r"`[^`]{3,}`"), "backtick-exec", "PROCESS_EXECUTION"),
221
+
222
+ # Category: File Deletion
223
+ (_re.compile(r"\b(os\.remove|os\.unlink|os\.rmdir)\s*\("), "os-delete", "FILE_DELETION"),
224
+ (_re.compile(r"\b(shutil\.rmtree|shutil\.move)\s*\("), "shutil-delete", "FILE_DELETION"),
225
+ (_re.compile(r"\bfs\.(unlink|rmdir|rm)(Sync)?\s*\("), "fs-delete", "FILE_DELETION"),
226
+ # Also match .unlinkSync( / .rmSync( / .rmdirSync( as method calls (e.g., require('fs').unlinkSync())
227
+ (_re.compile(r"\.(unlink|rmdir|rm)(Sync)?\s*\("), "fs-delete", "FILE_DELETION"),
228
+ (_re.compile(r"\bFile\.(delete|unlink)\s*\("), "file-delete", "FILE_DELETION"),
229
+ (_re.compile(r"\bunlink\s*\("), "unlink-call", "FILE_DELETION"),
230
+ (_re.compile(r"\brmtree\s*\("), "rmtree-call", "FILE_DELETION"),
231
+ (_re.compile(r"\bFileUtils\.rm"), "fileutils-rm", "FILE_DELETION"),
232
+ (_re.compile(r"pathlib\.Path\([^)]*\)\.(unlink|rmdir)"), "pathlib-delete", "FILE_DELETION"),
233
+
234
+ # Category: File Write
235
+ (_re.compile(r"open\s*\([^)]*['\"][wWaA]"), "file-write-open", "FILE_WRITE"),
236
+ (_re.compile(r"\bfs\.writeFile(Sync)?\s*\("), "fs-write", "FILE_WRITE"),
237
+ # Also match .writeFileSync( / .appendFileSync( as method calls
238
+ (_re.compile(r"\.writeFile(Sync)?\s*\("), "fs-write", "FILE_WRITE"),
239
+ (_re.compile(r"\bfs\.appendFile(Sync)?\s*\("), "fs-append", "FILE_WRITE"),
240
+ (_re.compile(r"\.appendFile(Sync)?\s*\("), "fs-append", "FILE_WRITE"),
241
+ (_re.compile(r"\bFile\.(write|open)\b.*['\"][wWaA]"), "file-write-ruby", "FILE_WRITE"),
242
+ (_re.compile(r"\.write\s*\("), "file-write", "FILE_WRITE"),
243
+ (_re.compile(r"pathlib\.Path\([^)]*\)\.(rename|write_)"), "pathlib-write", "FILE_WRITE"),
244
+
245
+ # Category: File System Mutation (os.rename, os.makedirs, shutil.copy)
246
+ (_re.compile(r"\bos\.rename\s*\("), "os-rename", "FILE_MUTATION"),
247
+ (_re.compile(r"\bos\.makedirs?\s*\("), "os-makedirs", "FILE_MUTATION"),
248
+ (_re.compile(r"\bshutil\.copy\s*\("), "shutil-copy", "FILE_MUTATION"),
249
+
250
+ # Category: Network
251
+ (_re.compile(r"\bhttps?://\S+"), "url-literal", "NETWORK"),
252
+ (_re.compile(r"\b(fetch|axios|requests\.get|urllib)\s*\("), "http-call", "NETWORK"),
253
+ (_re.compile(r"\bNet::HTTP\b"), "net-http", "NETWORK"),
254
+
255
+ # Category: Permission Modification
256
+ (_re.compile(r"\bos\.chmod\s*\("), "os-chmod", "PERMISSION_MOD"),
257
+ (_re.compile(r"\bfs\.chmod(Sync)?\s*\("), "fs-chmod", "PERMISSION_MOD"),
258
+ )
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # Layer 3: Heuristic safety classification
262
+ # ---------------------------------------------------------------------------
263
+ _SUSPICIOUS_HEURISTICS: Tuple[Tuple[_re.Pattern, str], ...] = (
264
+ (_re.compile(r"(/etc/|/home/|~/\.ssh|/var/|/usr/|/root/)"), "sensitive-path"),
265
+ (_re.compile(r"\b(base64|b64encode|b64decode|atob|btoa)\b"), "encoding"),
266
+ (_re.compile(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"), "ip-address"),
267
+ )
268
+
269
+ MAX_SAFE_INLINE_LENGTH = 150
270
+ MAX_NORMAL_INLINE_LENGTH = 500
271
+
272
+
273
+ # ============================================================================
274
+ # Command Aliases (single-token commands that map to a category)
275
+ # ============================================================================
276
+
277
+ # All command aliases are MUTATIVE (approvable via nonce).
278
+ # The truly destructive patterns (rm -rf /, dd of=/dev/sda, mkfs, fdisk) are
279
+ # permanently blocked by blocked_commands.py before the verb detector runs.
280
+ COMMAND_ALIASES: Dict[str, str] = {
281
+ "rm": CATEGORY_MUTATIVE,
282
+ "rmdir": CATEGORY_MUTATIVE,
283
+ "mv": CATEGORY_MUTATIVE,
284
+ "cp": CATEGORY_MUTATIVE,
285
+ "ln": CATEGORY_MUTATIVE,
286
+ "dd": CATEGORY_MUTATIVE,
287
+ "mkfs": CATEGORY_MUTATIVE,
288
+ "fdisk": CATEGORY_MUTATIVE,
289
+ "chmod": CATEGORY_MUTATIVE,
290
+ "chown": CATEGORY_MUTATIVE,
291
+ "chgrp": CATEGORY_MUTATIVE,
292
+ }
293
+
294
+
295
+ # ============================================================================
296
+ # Simulation Flags (--dry-run and equivalents)
297
+ # ============================================================================
298
+
299
+ SIMULATION_FLAGS: FrozenSet[str] = frozenset({
300
+ "--dry-run",
301
+ "--dryrun",
302
+ "--dry-run=client",
303
+ "--dry-run=server",
304
+ })
305
+
306
+
307
+ # ============================================================================
308
+ # Dangerous Flags (context-sensitive)
309
+ # ============================================================================
310
+
311
+ DANGEROUS_FLAGS: Dict[str, str] = {
312
+ "--force": "ALWAYS",
313
+ "--no-preserve-root": "ALWAYS",
314
+ "--force-with-lease": "ALWAYS",
315
+ "--prune": "ALWAYS",
316
+ "--cascade": "ALWAYS",
317
+ "--grace-period=0": "ALWAYS",
318
+ "--now": "ALWAYS",
319
+ "-f": "CONTEXT",
320
+ "-r": "CONTEXT",
321
+ "-R": "CONTEXT",
322
+ "-D": "CONTEXT",
323
+ "-M": "CONTEXT",
324
+ "--recursive": "CONTEXT",
325
+ "--delete": "CONTEXT",
326
+ "-rf": "ALWAYS",
327
+ "-fr": "ALWAYS",
328
+ }
329
+
330
+ # CLIs where -f means --force (not --file or --format)
331
+ F_FLAG_MEANS_FORCE: FrozenSet[str] = frozenset({
332
+ "rm", "cp", "mv", "ln", "docker", "podman",
333
+ "kubectl", "helm", "apt-get", "brew",
334
+ })
335
+
336
+ # CLIs where -r means recursive delete (not --region or --role)
337
+ R_FLAG_MEANS_RECURSIVE_DELETE: FrozenSet[str] = frozenset({
338
+ "rm", "cp", "chmod", "chown", "chgrp", "find",
339
+ "gsutil",
340
+ })
341
+
342
+ # CLIs where -D means force-delete (not -D for other meanings)
343
+ D_FLAG_MEANS_FORCE_DELETE: FrozenSet[str] = frozenset({
344
+ "git",
345
+ })
346
+
347
+ # CLIs where -M means force-move/rename (not -M for other meanings)
348
+ M_FLAG_MEANS_FORCE_MOVE: FrozenSet[str] = frozenset({
349
+ "git",
350
+ })
351
+
352
+ # CLIs where --delete is a destructive flag (not a query filter)
353
+ DELETE_FLAG_IS_DESTRUCTIVE: FrozenSet[str] = frozenset({
354
+ "git", "rsync",
355
+ })
356
+
357
+
358
+ # ============================================================================
359
+ # Lightweight CLI Family Lookup (metadata only, not routing)
360
+ # ============================================================================
361
+
362
+ CLI_FAMILY_LOOKUP: Dict[str, str] = {
363
+ "kubectl": "k8s", "helm": "k8s", "flux": "k8s", "kustomize": "k8s",
364
+ "k9s": "k8s", "kubectx": "k8s", "kubens": "k8s", "stern": "k8s",
365
+ "terraform": "iac", "terragrunt": "iac", "pulumi": "iac", "cdktf": "iac",
366
+ "git": "git",
367
+ "docker": "docker", "podman": "docker",
368
+ "docker-compose": "docker", "podman-compose": "docker",
369
+ "aws": "cloud", "gcloud": "cloud", "gsutil": "cloud", "az": "cloud",
370
+ "eksctl": "cloud", "gh": "cloud", "glab": "cloud",
371
+ "vercel": "cloud", "netlify": "cloud",
372
+ "fly": "cloud", "flyctl": "cloud", "heroku": "cloud",
373
+ "npm": "package", "npx": "package", "pnpm": "package",
374
+ "yarn": "package", "bun": "package", "deno": "package",
375
+ "pip": "package", "pip3": "package", "poetry": "package",
376
+ "pipenv": "package", "uv": "package",
377
+ "apt": "package", "apt-get": "package", "brew": "package",
378
+ "cargo": "package", "go": "package",
379
+ "make": "build", "cmake": "build", "bazel": "build",
380
+ "gradle": "build", "mvn": "build",
381
+ "node": "runtime", "python": "runtime", "python3": "runtime",
382
+ "tsx": "runtime", "ts-node": "runtime",
383
+ "pytest": "linter", "mypy": "linter", "black": "linter",
384
+ "ruff": "linter", "flake8": "linter", "pylint": "linter",
385
+ "systemctl": "system", "service": "system", "supervisorctl": "system",
386
+ }
387
+
388
+
389
+ # ============================================================================
390
+ # Dangerous Flag Scanning
391
+ # ============================================================================
392
+
393
+ def _scan_dangerous_flags(
394
+ tokens: List[str] | tuple,
395
+ cli: str,
396
+ ) -> Tuple[str, ...]:
397
+ """Scan tokens for dangerous flags with context sensitivity.
398
+
399
+ Context rules:
400
+ - "-f" is only dangerous if cli is in F_FLAG_MEANS_FORCE
401
+ - "-r"/"-R" is only dangerous if cli is in R_FLAG_MEANS_RECURSIVE_DELETE
402
+ - "-D" is only dangerous if cli is in D_FLAG_MEANS_FORCE_DELETE
403
+ - "-M" is only dangerous if cli is in M_FLAG_MEANS_FORCE_MOVE
404
+ - "--delete" is only dangerous if cli is in DELETE_FLAG_IS_DESTRUCTIVE
405
+ - Compound flags like "-rf" are always dangerous
406
+
407
+ Args:
408
+ tokens: Tokenized command.
409
+ cli: CLI tool name.
410
+
411
+ Returns:
412
+ Tuple of dangerous flag strings found.
413
+ """
414
+ found: List[str] = []
415
+
416
+ for token in tokens:
417
+ if not token.startswith("-"):
418
+ continue
419
+
420
+ # Check exact matches in DANGEROUS_FLAGS
421
+ if token in DANGEROUS_FLAGS:
422
+ flag_type = DANGEROUS_FLAGS[token]
423
+
424
+ if flag_type == "ALWAYS":
425
+ found.append(token)
426
+ continue
427
+
428
+ # CONTEXT-sensitive flags
429
+ if token == "-f":
430
+ if cli in F_FLAG_MEANS_FORCE:
431
+ found.append(token)
432
+ elif token in ("-r", "-R"):
433
+ if cli in R_FLAG_MEANS_RECURSIVE_DELETE:
434
+ found.append(token)
435
+ elif token == "-D":
436
+ if cli in D_FLAG_MEANS_FORCE_DELETE:
437
+ found.append(token)
438
+ elif token == "-M":
439
+ if cli in M_FLAG_MEANS_FORCE_MOVE:
440
+ found.append(token)
441
+ elif token == "--delete":
442
+ if cli in DELETE_FLAG_IS_DESTRUCTIVE:
443
+ found.append(token)
444
+ elif token == "--recursive":
445
+ if cli in R_FLAG_MEANS_RECURSIVE_DELETE:
446
+ found.append(token)
447
+
448
+ # Check for compound short flags containing dangerous combos
449
+ # e.g., "-rfi" contains both -r and -f
450
+ elif len(token) > 2 and token[0] == "-" and token[1] != "-":
451
+ flag_chars = token[1:]
452
+ if "r" in flag_chars and "f" in flag_chars:
453
+ found.append(token)
454
+ elif "f" in flag_chars and cli in F_FLAG_MEANS_FORCE:
455
+ found.append(token)
456
+ elif "r" in flag_chars and cli in R_FLAG_MEANS_RECURSIVE_DELETE:
457
+ found.append(token)
458
+
459
+ return tuple(found)
460
+
461
+
462
+ # ============================================================================
463
+ # Main Detection Function
464
+ # ============================================================================
465
+
466
+ @functools.lru_cache(maxsize=128)
467
+ def detect_mutative_command(command: str) -> MutativeResult:
468
+ """Analyze a shell command and return a structured mutative assessment.
469
+
470
+ Simplified algorithm (CLI-agnostic):
471
+ 1. Tokenize the command.
472
+ 2. COMMAND_ALIASES fast-path.
473
+ 3. Simulation flag override: --dry-run anywhere = not mutative.
474
+ 4. Scan the first semantic non-flag tokens after the base CLI.
475
+ 5. Scan for dangerous flags.
476
+ 6. No match: not mutative (safe by elimination).
477
+
478
+ Args:
479
+ command: Raw shell command string.
480
+
481
+ Returns:
482
+ MutativeResult with full classification details.
483
+ """
484
+ # --- Edge case: empty command ---
485
+ if not command or not command.strip():
486
+ return MutativeResult(
487
+ is_mutative=False,
488
+ category=CATEGORY_UNKNOWN,
489
+ reason="Empty command",
490
+ confidence="high",
491
+ )
492
+
493
+ semantics = analyze_command(command)
494
+ tokens = list(semantics.tokens)
495
+ if not tokens:
496
+ return MutativeResult(
497
+ is_mutative=False,
498
+ category=CATEGORY_UNKNOWN,
499
+ reason="No tokens after parsing",
500
+ confidence="high",
501
+ )
502
+
503
+ base_cmd = semantics.base_cmd
504
+ family = CLI_FAMILY_LOOKUP.get(base_cmd, "unknown")
505
+
506
+ # --- Step 1: Command alias fast-path ---
507
+ if base_cmd in COMMAND_ALIASES:
508
+ alias_category = COMMAND_ALIASES[base_cmd]
509
+ dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
510
+ return MutativeResult(
511
+ is_mutative=True,
512
+ category=alias_category,
513
+ verb=base_cmd,
514
+ dangerous_flags=dangerous_flags,
515
+ cli_family=family if family != "unknown" else "system",
516
+ confidence="high",
517
+ reason=f"Command alias '{base_cmd}' is {alias_category.lower()}",
518
+ )
519
+
520
+ # --- Step 2: Single-token command (no verb to extract) ---
521
+ if len(tokens) == 1:
522
+ return MutativeResult(
523
+ is_mutative=False,
524
+ category=CATEGORY_UNKNOWN,
525
+ verb=base_cmd,
526
+ cli_family=family,
527
+ confidence="low",
528
+ reason=f"Single-token command '{base_cmd}' with no verb",
529
+ )
530
+
531
+ # --- Step 3: Simulation flag override ---
532
+ if any(t.lower() in SIMULATION_FLAGS for t in tokens):
533
+ # Find the first non-flag token after base_cmd for the verb
534
+ verb, _ = _find_first_non_flag(semantics.semantic_head_tokens)
535
+ return MutativeResult(
536
+ is_mutative=False,
537
+ category=CATEGORY_SIMULATION,
538
+ verb=verb,
539
+ cli_family=family,
540
+ confidence="high",
541
+ reason=f"Simulation flag detected (command has --dry-run or equivalent)",
542
+ )
543
+
544
+ # --- Step 3b: Inline code safety check (python3 -c, node -e, etc.) ---
545
+ # For runtime interpreters with inline code flags, scan the code string
546
+ # using the 3-layer approach instead of verb-matching tokens (which would
547
+ # false-positive on generic keywords like "import", "create", etc.).
548
+ cli_flags = _INLINE_CODE_MAP.get(base_cmd, frozenset())
549
+ if base_cmd in _INLINE_CODE_CLIS and cli_flags & set(semantics.flag_tokens):
550
+ return _check_inline_code(command, base_cmd, family)
551
+
552
+ # --- Step 4: Scan semantic non-flag tokens near the command head ---
553
+ # Priority order: SIMULATION > MUTATIVE > READ_ONLY > ALIASES
554
+ for semantic_index, token in enumerate(semantics.semantic_head_tokens[1:], start=1):
555
+ # Check compound read-only subcommands BEFORE hyphen-split.
556
+ # Without this, "merge-base" would be split to "merge" -> MUTATIVE.
557
+ if token in COMPOUND_READ_ONLY_SUBCOMMANDS:
558
+ return MutativeResult(
559
+ is_mutative=False,
560
+ category=CATEGORY_READ_ONLY,
561
+ verb=token,
562
+ cli_family=family,
563
+ confidence="high",
564
+ reason=f"Compound read-only subcommand '{token}'",
565
+ )
566
+
567
+ # Split hyphenated tokens: "delete-stack" -> check "delete"
568
+ candidate = token.split("-", 1)[0] if "-" in token else token
569
+
570
+ # Also check full token for exact matches (e.g., "force-delete")
571
+ full_lower = token
572
+
573
+ # Determine confidence from position
574
+ confidence = "high" if semantic_index <= 2 else "medium"
575
+
576
+ # Check verb taxonomy in priority order
577
+ if candidate in SIMULATION_VERBS or full_lower in SIMULATION_VERBS:
578
+ verb = candidate if candidate in SIMULATION_VERBS else full_lower
579
+ return MutativeResult(
580
+ is_mutative=False,
581
+ category=CATEGORY_SIMULATION,
582
+ verb=verb,
583
+ cli_family=family,
584
+ confidence=confidence,
585
+ reason=f"Simulation verb '{verb}'",
586
+ )
587
+
588
+ if candidate in MUTATIVE_VERBS or full_lower in MUTATIVE_VERBS:
589
+ verb = candidate if candidate in MUTATIVE_VERBS else full_lower
590
+
591
+ # Check verb+flag overrides: some verbs become READ_ONLY with
592
+ # specific flags (e.g., "git tag -l" is listing, not creating).
593
+ override_key = (family, verb)
594
+ if override_key in VERB_FLAG_READ_ONLY_OVERRIDES:
595
+ override_flags = VERB_FLAG_READ_ONLY_OVERRIDES[override_key]
596
+ if override_flags & frozenset(semantics.flag_tokens):
597
+ return MutativeResult(
598
+ is_mutative=False,
599
+ category=CATEGORY_READ_ONLY,
600
+ verb=verb,
601
+ cli_family=family,
602
+ confidence="high",
603
+ reason=f"Verb '{verb}' overridden to read-only by flag",
604
+ )
605
+
606
+ dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
607
+ flag_detail = (
608
+ f" with dangerous flags {dangerous_flags}"
609
+ if dangerous_flags else ""
610
+ )
611
+ return MutativeResult(
612
+ is_mutative=True,
613
+ category=CATEGORY_MUTATIVE,
614
+ verb=verb,
615
+ dangerous_flags=dangerous_flags,
616
+ cli_family=family,
617
+ confidence=confidence,
618
+ reason=f"Mutative verb '{verb}'{flag_detail}",
619
+ )
620
+
621
+ if candidate in READ_ONLY_VERBS or full_lower in READ_ONLY_VERBS:
622
+ verb = candidate if candidate in READ_ONLY_VERBS else full_lower
623
+ return MutativeResult(
624
+ is_mutative=False,
625
+ category=CATEGORY_READ_ONLY,
626
+ verb=verb,
627
+ cli_family=family,
628
+ confidence=confidence,
629
+ reason=f"Read-only verb '{verb}'",
630
+ )
631
+
632
+ # Check command aliases as verb (e.g., "docker rm" -> rm is alias)
633
+ if candidate in COMMAND_ALIASES:
634
+ alias_cat = COMMAND_ALIASES[candidate]
635
+ dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
636
+ return MutativeResult(
637
+ is_mutative=True,
638
+ category=alias_cat,
639
+ verb=candidate,
640
+ dangerous_flags=dangerous_flags,
641
+ cli_family=family,
642
+ confidence=confidence,
643
+ reason=f"Verb alias '{candidate}' is {alias_cat.lower()}",
644
+ )
645
+
646
+ # --- Step 4b: API subcommand with no explicit mutative HTTP method ---
647
+ # CLIs like `gh api` and `glab api` default to GET when no -X flag is
648
+ # specified. If the semantic scan found no verb and the subcommand is
649
+ # "api", treat the command as read-only.
650
+ if (
651
+ not any(
652
+ t in MUTATIVE_VERBS
653
+ for t in semantics.semantic_head_tokens[1:]
654
+ )
655
+ and len(semantics.semantic_head_tokens) > 1
656
+ and semantics.semantic_head_tokens[1] == "api"
657
+ ):
658
+ return MutativeResult(
659
+ is_mutative=False,
660
+ category=CATEGORY_READ_ONLY,
661
+ verb="api",
662
+ cli_family=family,
663
+ confidence="high",
664
+ reason="API call with implicit GET method",
665
+ )
666
+
667
+ # --- Step 5: Scan for dangerous flags (no verb found) ---
668
+ dangerous_flags = _scan_dangerous_flags(tokens, base_cmd)
669
+ if dangerous_flags:
670
+ # Find first non-flag token as the "verb" for reporting
671
+ verb, _ = _find_first_non_flag(semantics.semantic_head_tokens)
672
+ return MutativeResult(
673
+ is_mutative=True,
674
+ category=CATEGORY_UNKNOWN,
675
+ verb=verb,
676
+ dangerous_flags=dangerous_flags,
677
+ cli_family=family,
678
+ confidence="low",
679
+ reason=f"Unknown verb '{verb}' with dangerous flags {dangerous_flags}",
680
+ )
681
+
682
+ # --- Step 6: No match -- not mutative (safe by elimination) ---
683
+ verb, _ = _find_first_non_flag(semantics.semantic_head_tokens)
684
+ return MutativeResult(
685
+ is_mutative=False,
686
+ category=CATEGORY_UNKNOWN,
687
+ verb=verb,
688
+ cli_family=family,
689
+ confidence="low",
690
+ reason=f"Unknown verb '{verb}' with no dangerous flags",
691
+ )
692
+
693
+
694
+ # ============================================================================
695
+ # Helpers
696
+ # ============================================================================
697
+
698
+ def _check_inline_code(command: str, base_cmd: str, family: str) -> MutativeResult:
699
+ """Check inline code for dangerous patterns using a 3-layer approach.
700
+
701
+ Layer 1: Extract string literals from inline code and check them against
702
+ blocked_commands (catches embedded shell commands like 'rm -rf /').
703
+ Layer 2: Scan for universal dangerous API keywords (language-agnostic).
704
+ Layer 3: Heuristic safety classification (length, sensitive paths, encoding).
705
+
706
+ Args:
707
+ command: Full raw command string.
708
+ base_cmd: The interpreter (e.g., "python3", "node", "ruby").
709
+ family: CLI family hint.
710
+
711
+ Returns:
712
+ MutativeResult -- MUTATIVE if any layer triggers, else safe.
713
+ """
714
+ # ---- Layer 1: Extract string literals → check against blocked_commands ----
715
+ if _is_blocked_command is not None:
716
+ embedded_strings = _extract_embedded_shell_commands(command)
717
+ for literal in embedded_strings:
718
+ blocked = _is_blocked_command(literal)
719
+ if blocked.is_blocked:
720
+ return MutativeResult(
721
+ is_mutative=True,
722
+ category=CATEGORY_MUTATIVE,
723
+ verb="embedded-blocked-cmd",
724
+ cli_family=family,
725
+ confidence="high",
726
+ reason=(
727
+ f"Inline code contains blocked shell command in "
728
+ f"string literal: {blocked.category}"
729
+ ),
730
+ )
731
+
732
+ # ---- Layer 2: Universal dangerous API keyword patterns ----
733
+ for pattern, label, category in _UNIVERSAL_DANGEROUS_PATTERNS:
734
+ if pattern.search(command):
735
+ return MutativeResult(
736
+ is_mutative=True,
737
+ category=CATEGORY_MUTATIVE,
738
+ verb=label,
739
+ cli_family=family,
740
+ confidence="medium",
741
+ reason=f"Inline code contains dangerous pattern: {label} ({category})",
742
+ )
743
+
744
+ # ---- Layer 3: Heuristic safety classification ----
745
+ # 3a: Check for suspicious indicators (sensitive paths, encoding, IPs)
746
+ for pattern, label in _SUSPICIOUS_HEURISTICS:
747
+ if pattern.search(command):
748
+ return MutativeResult(
749
+ is_mutative=True,
750
+ category=CATEGORY_MUTATIVE,
751
+ verb=f"heuristic-{label}",
752
+ cli_family=family,
753
+ confidence="low",
754
+ reason=f"Inline code flagged by heuristic: {label}",
755
+ )
756
+
757
+ # 3b: Unusually long inline code is suspicious
758
+ # Extract the code portion after the inline flag for length check.
759
+ # Use a rough extraction: everything after the first inline flag.
760
+ code_portion = command
761
+ cli_flag_tokens = _INLINE_CODE_MAP.get(base_cmd, frozenset())
762
+ for flag in cli_flag_tokens:
763
+ idx = command.find(f" {flag} ")
764
+ if idx != -1:
765
+ code_portion = command[idx + len(flag) + 2:]
766
+ break
767
+
768
+ if len(code_portion) > MAX_NORMAL_INLINE_LENGTH:
769
+ return MutativeResult(
770
+ is_mutative=True,
771
+ category=CATEGORY_MUTATIVE,
772
+ verb="heuristic-long-code",
773
+ cli_family=family,
774
+ confidence="low",
775
+ reason=(
776
+ f"Inline code is unusually long ({len(code_portion)} chars > "
777
+ f"{MAX_NORMAL_INLINE_LENGTH} limit)"
778
+ ),
779
+ )
780
+
781
+ # ---- No layers triggered -- safe inline code ----
782
+ return MutativeResult(
783
+ is_mutative=False,
784
+ category=CATEGORY_READ_ONLY,
785
+ verb="inline-code",
786
+ cli_family=family,
787
+ confidence="medium",
788
+ reason=f"Inline code ({base_cmd}) with no dangerous patterns",
789
+ )
790
+
791
+
792
+ def _find_first_non_flag(tokens: List[str] | tuple) -> tuple:
793
+ """Find the first semantic token after tokens[0].
794
+
795
+ Returns:
796
+ (verb, position) tuple. ("", -1) if no non-flag token found.
797
+ """
798
+ for i in range(1, len(tokens)):
799
+ if tokens[i]:
800
+ return tokens[i], i
801
+ return "", -1
802
+
803
+
804
+ # ============================================================================
805
+ # Hook Response Builder
806
+ # ============================================================================
807
+
808
+ def build_t3_block_response(
809
+ command: str,
810
+ danger: MutativeResult,
811
+ nonce: str = "",
812
+ ) -> dict:
813
+ """Build an internal block response dict for T3 commands.
814
+
815
+ Returns an internal dict consumed by bash_validator, which wraps the
816
+ 'message' field into a hookSpecificOutput with permissionDecision: "deny".
817
+ The 'decision' key is internal only and never sent to Claude Code.
818
+
819
+ Args:
820
+ command: The original shell command.
821
+ danger: MutativeResult from detect_mutative_command.
822
+ nonce: Cryptographic nonce for this pending approval. When provided,
823
+ the block message includes the approval code that the agent must
824
+ present to the user.
825
+
826
+ Returns:
827
+ Dict with 'decision' (internal) and 'message' (forwarded to agent) keys.
828
+ """
829
+ flag_warning = ""
830
+ if danger.dangerous_flags:
831
+ flag_warning = (
832
+ f"\nDangerous flags detected: {', '.join(danger.dangerous_flags)}"
833
+ )
834
+
835
+ message = (
836
+ f"[T3_APPROVAL_REQUIRED] {danger.category} operation detected.\n"
837
+ f"Command: {command}\n"
838
+ f"Verb: '{danger.verb}' (CLI family: {danger.cli_family})\n"
839
+ f"Confidence: {danger.confidence}\n"
840
+ f"Reason: {danger.reason}{flag_warning}\n"
841
+ f"\n"
842
+ f"{build_t3_approval_instructions(nonce)}"
843
+ )
844
+
845
+ return {
846
+ "decision": "block",
847
+ "message": message,
848
+ }
849
+
850
+