@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,1008 @@
1
+ """
2
+ Bash command validator.
3
+
4
+ Primary security gate for all Bash tool invocations. With Bash(*) in the
5
+ settings.json allow list, ALL commands reach this hook -- it is the sole
6
+ enforcement layer for dangerous command detection.
7
+
8
+ 5-Phase Pipeline:
9
+ 1. UNWRAP -- ShellUnwrapper strips wrapper shells (bash -c, sh -c, ...);
10
+ depth >= _OBFUSCATION_DEPTH_LIMIT = permanent block.
11
+ Existing _detect_indirect_execution() runs as fallback for
12
+ eval, python -c, node -e, etc.
13
+ 2. DECOMPOSE -- StageDecomposer splits into operator-linked stages.
14
+ 3. CLASSIFY -- blocked_commands + cloud_pipe_validator + mutative_verbs
15
+ per stage (existing logic, unchanged).
16
+ 4. COMPOSITION -- cross-stage composition rules (exfiltration, RCE,
17
+ obfuscated exec via pipe analysis).
18
+ 5. AGGREGATE -- combine stage results into final BashValidationResult.
19
+
20
+ Earlier flat-pipeline order preserved within phases for backward compat:
21
+ - Footer stripping runs before phase 1 (EARLY NORMALIZATION)
22
+ - Indirect execution detection is the first check in phase 1
23
+ - Blocked commands run before cloud_pipe and mutative_verbs in phase 3
24
+ """
25
+
26
+ import os
27
+ import re
28
+ import json
29
+ import logging
30
+ from typing import Dict, Any, Optional, List
31
+ from dataclasses import dataclass
32
+
33
+ from ..security.tiers import SecurityTier
34
+ from ..security.blocked_commands import is_blocked_command
35
+ from ..security.gitops_validator import validate_gitops_workflow
36
+ from ..security.mutative_verbs import (
37
+ detect_mutative_command,
38
+ build_t3_block_response,
39
+ )
40
+ from ..security.flag_classifiers import (
41
+ classify_by_flags,
42
+ OUTCOME_BLOCKED as FLAG_BLOCKED,
43
+ OUTCOME_MUTATIVE as FLAG_MUTATIVE,
44
+ )
45
+ from ..security.approval_grants import (
46
+ check_approval_grant,
47
+ confirm_grant,
48
+ find_pending_for_command,
49
+ generate_nonce,
50
+ last_check_found_expired,
51
+ write_pending_approval,
52
+ )
53
+ from ..security.approval_messages import (
54
+ build_pending_approval_unavailable_message,
55
+ build_t3_approval_instructions,
56
+ )
57
+ from ..security.shell_unwrapper import ShellUnwrapper
58
+ from ..security.composition_rules import (
59
+ build_composition_stages,
60
+ check_composition,
61
+ CompositionDecision,
62
+ )
63
+ from .shell_parser import get_shell_parser
64
+ from .cloud_pipe_validator import validate_cloud_pipe
65
+ from .hook_response import build_hook_permission_response
66
+ from .stage_decomposer import StageDecomposer, DecomposedCommand
67
+
68
+ logger = logging.getLogger(__name__)
69
+
70
+ # Maximum wrapper depth before treating as obfuscation (permanent block).
71
+ _OBFUSCATION_DEPTH_LIMIT = 5
72
+
73
+
74
+ @dataclass
75
+ class BashValidationResult:
76
+ """Result of Bash command validation."""
77
+ allowed: bool
78
+ tier: SecurityTier
79
+ reason: str
80
+ suggestions: List[str] = None
81
+ modified_input: Optional[Dict[str, Any]] = None
82
+ # When set, the caller should return this dict (exit 0) instead of a
83
+ # plain error string (exit 2). Used for structured block responses that
84
+ # should correct the agent rather than terminate execution.
85
+ block_response: Optional[Dict[str, Any]] = None
86
+
87
+ def __post_init__(self):
88
+ if self.suggestions is None:
89
+ self.suggestions = []
90
+
91
+
92
+ # Patterns for AI tool attribution footers (auto-stripped from commits).
93
+ # Covers Claude Code, GitHub Copilot, Aider, Windsurf, and any future
94
+ # tool using the Co-authored-by git trailer convention.
95
+ FORBIDDEN_FOOTER_PATTERNS = [
96
+ r"Generated with\s+Claude Code",
97
+ r"Generated with\s+\[?Claude Code\]?",
98
+ r"Co-Authored-By:\s+Claude\b",
99
+ r"Co-authored-by:\s+GitHub Copilot\b",
100
+ r"Co-authored-by:\s+aider\b",
101
+ r"Co-authored-by:\s+Windsurf\b",
102
+ r"Co-authored-by:\s+Cursor\b",
103
+ r"Co-authored-by:\s+Codex\b",
104
+ r"Co-authored-by:\s+Gemini\b",
105
+ ]
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Indirect execution wrappers — commands that execute arbitrary strings.
109
+ # These bypass regex-based command blocking because the real command is
110
+ # hidden inside a string argument. Classified as T2 (requires approval)
111
+ # so the user sees what will actually run.
112
+ # ---------------------------------------------------------------------------
113
+ # Optional prefix commands that can wrap any shell invocation.
114
+ # nohup, sudo, env, nice, etc. — the regex allows zero or more of these
115
+ # before the real interpreter token so "nohup bash -c ..." is still caught.
116
+ _WRAPPER_PREFIX = r"(?:(?:nohup|sudo|env|nice|ionice|setsid|strace|ltrace|time)\s+)*"
117
+
118
+ INDIRECT_EXEC_PATTERNS = [
119
+ re.compile(r"^" + _WRAPPER_PREFIX + r"bash\s+-c\s+", re.IGNORECASE),
120
+ re.compile(r"^" + _WRAPPER_PREFIX + r"sh\s+-c\s+", re.IGNORECASE),
121
+ re.compile(r"^" + _WRAPPER_PREFIX + r"zsh\s+-c\s+", re.IGNORECASE),
122
+ re.compile(r"^" + _WRAPPER_PREFIX + r"dash\s+-c\s+", re.IGNORECASE),
123
+ re.compile(r"^\s*eval\s+", re.IGNORECASE),
124
+ re.compile(r"^" + _WRAPPER_PREFIX + r"python3?\s+-c\s+", re.IGNORECASE),
125
+ re.compile(r"^" + _WRAPPER_PREFIX + r"node\s+-e\s+", re.IGNORECASE),
126
+ re.compile(r"^" + _WRAPPER_PREFIX + r"perl\s+-e\s+", re.IGNORECASE),
127
+ re.compile(r"^" + _WRAPPER_PREFIX + r"ruby\s+-e\s+", re.IGNORECASE),
128
+ # Process substitution and heredoc piped to shell
129
+ re.compile(r"^" + _WRAPPER_PREFIX + r"bash\s+<\(", re.IGNORECASE),
130
+ re.compile(r"^" + _WRAPPER_PREFIX + r"sh\s+<\(", re.IGNORECASE),
131
+ ]
132
+
133
+ class BashValidator:
134
+ """Validator for Bash tool invocations.
135
+
136
+ Implements a 5-phase pipeline: unwrap -> decompose -> classify ->
137
+ composition -> aggregate. See module docstring for phase details.
138
+ """
139
+
140
+ def __init__(self):
141
+ """Initialize validator with parser, unwrapper, and decomposer."""
142
+ self.shell_parser = get_shell_parser()
143
+ self._unwrapper = ShellUnwrapper()
144
+ self._decomposer = StageDecomposer()
145
+
146
+ def _detect_indirect_execution(self, command: str) -> Optional[BashValidationResult]:
147
+ """Detect indirect execution wrappers that can bypass regex blocking.
148
+
149
+ Commands like 'bash -c "az group delete"' hide the real command inside
150
+ a string. We classify these as T2 (mutative) so they require user
151
+ approval via the nonce workflow, giving the human a chance to inspect
152
+ what will actually run.
153
+
154
+ Returns BashValidationResult if indirect execution detected, else None.
155
+ """
156
+ for pattern in INDIRECT_EXEC_PATTERNS:
157
+ if pattern.search(command):
158
+ # Also check if the inner payload contains a blocked command.
159
+ # Extract the string argument after the wrapper.
160
+ inner = self._extract_inner_command(command)
161
+ if inner:
162
+ blocked = is_blocked_command(inner)
163
+ if blocked.is_blocked:
164
+ return BashValidationResult(
165
+ allowed=False,
166
+ tier=SecurityTier.T3_BLOCKED,
167
+ reason=(
168
+ f"Indirect execution of blocked command detected: "
169
+ f"{blocked.category} (via wrapper)"
170
+ ),
171
+ suggestions=[
172
+ blocked.suggestion or "Run the command directly instead of via a shell wrapper.",
173
+ ],
174
+ )
175
+
176
+ # Not blocked but still indirect — route through approval
177
+ logger.info("Indirect execution detected: %s", command[:80])
178
+ result = detect_mutative_command(command)
179
+ if result.is_mutative:
180
+ return None # Already mutative, will be caught by mutative_verbs
181
+
182
+ # For interpreters with inline code analysis (python3 -c),
183
+ # mutative_verbs.py has dedicated pattern scanning that
184
+ # distinguishes safe code (json.dumps, sys.version) from
185
+ # dangerous code (os.system, subprocess.run). If it classified
186
+ # the inline code as safe, trust that analysis and allow it
187
+ # through without forcing an "ask" dialog.
188
+ from ..security.mutative_verbs import _INLINE_CODE_CLIS
189
+ base_cmd = command.strip().split()[0].rsplit("/", 1)[-1].lower()
190
+ if base_cmd in _INLINE_CODE_CLIS:
191
+ logger.info(
192
+ "Inline code classified as safe by pattern scanner: %s",
193
+ command[:80],
194
+ )
195
+ return None # Safe inline code, proceed to normal validation
196
+
197
+ # Shell wrappers (bash -c, eval, etc.) hide the real command
198
+ # in a string — no dedicated scanner exists. Force "ask" so
199
+ # the user can inspect what will actually run.
200
+ #
201
+ # Inspect the inner command to identify the mutative verb so
202
+ # the user sees a more informative message
203
+ # (e.g. "inner mutative verb 'mv'"). Falls back to generic
204
+ # message when inner has no mutative verb.
205
+ reason_msg = "Indirect execution wrapper detected — requires confirmation"
206
+ if inner:
207
+ inner_result = detect_mutative_command(inner)
208
+ if inner_result.is_mutative and inner_result.verb:
209
+ reason_msg = (
210
+ f"Indirect execution detected: inner mutative verb "
211
+ f"'{inner_result.verb}' — requires confirmation"
212
+ )
213
+ dialog_msg = (
214
+ "Indirect execution detected. The command uses a shell "
215
+ "wrapper (bash -c, eval, etc.) that can bypass "
216
+ "security checks. Please confirm you want to run this."
217
+ )
218
+ hook_block = build_hook_permission_response("ask", dialog_msg)
219
+ return BashValidationResult(
220
+ allowed=False,
221
+ tier=SecurityTier.T2_DRY_RUN,
222
+ reason=reason_msg,
223
+ block_response=hook_block,
224
+ )
225
+ return None
226
+
227
+ def _extract_inner_command(self, command: str) -> Optional[str]:
228
+ """Extract the inner command from an indirect execution wrapper.
229
+
230
+ E.g., 'bash -c "az group delete --name foo"' → 'az group delete --name foo'
231
+ """
232
+ # Match: shell -c "..." or shell -c '...'
233
+ match = re.search(r"""-[ce]\s+(['"])(.*?)\1""", command, re.DOTALL)
234
+ if match:
235
+ return match.group(2).strip()
236
+ # Match: shell -c ... (unquoted, take rest of line)
237
+ match = re.search(r"-[ce]\s+(\S+.*)", command)
238
+ if match:
239
+ return match.group(1).strip()
240
+ return None
241
+
242
+ def _has_operators(self, command: str) -> bool:
243
+ """Quick check if command has operators (before parsing).
244
+
245
+ Detects pipes, logical operators, semicolons, redirects, and
246
+ background operators. This is a fast pre-filter — the full
247
+ shell parser handles quote-aware splitting downstream.
248
+
249
+ Note: '>' and '&' are included so commands with redirects or
250
+ background operators reach the compound path, where the
251
+ sanitization layer can strip them.
252
+ """
253
+ # Fast check for common operators outside quotes
254
+ # This avoids expensive parsing for 70% of commands
255
+ if not any(op in command for op in ['|', '&&', '||', ';', '\n', '>', '&']):
256
+ return False
257
+ return True
258
+
259
+ # Regex patterns for operators that can be safely stripped from commands.
260
+ # Applied after quote-masking to avoid false positives.
261
+ _NOHUP_PREFIX_RE = re.compile(r"^\s*nohup\s+")
262
+ _TRAILING_BG_RE = re.compile(r"\s*&\s*$")
263
+ _REDIRECT_RE = re.compile(r"\s*>{1,2}\s*\S+\s*$")
264
+ # Fd duplication (2>&1) is harmless and should NOT be stripped.
265
+ _FD_DUP_RE = re.compile(r"\d+>&\d+")
266
+
267
+ def _try_sanitize_command(self, command: str) -> Optional[BashValidationResult]:
268
+ """Attempt to strip dangerous operators and return a clean command.
269
+
270
+ Sanitizable patterns (can be stripped without changing semantics):
271
+ - nohup prefix: ``nohup cmd args`` -> ``cmd args``
272
+ - trailing &: ``cmd args &`` -> ``cmd args``
273
+ - trailing redirect: ``cmd args > file`` -> ``cmd args``
274
+
275
+ Non-sanitizable patterns (reject with guidance):
276
+ - Pipes (change data flow between commands)
277
+ - Chaining operators (&&, ||, ;) — use one-command-per-step
278
+
279
+ Returns:
280
+ BashValidationResult with cleaned command via modified_input if
281
+ sanitization succeeded, or a block response if it cannot be cleaned.
282
+ None if no sanitization is needed (command has no dangerous operators).
283
+ """
284
+ original = command
285
+ cleaned = command
286
+ stripped_parts = []
287
+
288
+ # Strip nohup prefix
289
+ if self._NOHUP_PREFIX_RE.match(cleaned):
290
+ cleaned = self._NOHUP_PREFIX_RE.sub("", cleaned).strip()
291
+ stripped_parts.append("nohup")
292
+
293
+ # Strip trailing & (background) but not && or >&
294
+ # Mask fd duplications first to avoid false matching
295
+ test_str = self._FD_DUP_RE.sub("", cleaned)
296
+ if self._TRAILING_BG_RE.search(test_str):
297
+ cleaned = self._TRAILING_BG_RE.sub("", cleaned).strip()
298
+ stripped_parts.append("&")
299
+
300
+ # Strip trailing redirect (> file or >> file)
301
+ # Only strip if it's at the end of the command
302
+ test_str = self._FD_DUP_RE.sub("", cleaned)
303
+ redirect_match = self._REDIRECT_RE.search(test_str)
304
+ if redirect_match:
305
+ # Find the position in the original cleaned string
306
+ # We need to remove from the redirect operator onward
307
+ pos = cleaned.rfind(">")
308
+ if pos > 0:
309
+ before_redirect = cleaned[:pos].rstrip()
310
+ # Only strip if the > is not inside a flag value like --output=>
311
+ if before_redirect and not before_redirect.endswith("="):
312
+ cleaned = before_redirect
313
+ stripped_parts.append("> redirect")
314
+
315
+ if not stripped_parts:
316
+ return None # Nothing to sanitize
317
+
318
+ if cleaned == original:
319
+ return None # Sanitization didn't change anything
320
+
321
+ logger.info(
322
+ "Command sanitized: stripped [%s] from: %s",
323
+ ", ".join(stripped_parts),
324
+ original[:80],
325
+ )
326
+
327
+ # Build the response with the cleaned command via updatedInput
328
+ reason = (
329
+ f"Command sanitized: stripped {', '.join(stripped_parts)}. "
330
+ f"Read the command-execution skill for proper patterns.\n"
331
+ f"Original: {original[:120]}\n"
332
+ f"Cleaned: {cleaned[:120]}"
333
+ )
334
+ hook_response = build_hook_permission_response(
335
+ "allow", reason, updated_input={"command": cleaned}
336
+ )
337
+ # Inject updatedInput into the response for the hook entry point
338
+ hook_response.setdefault("hookSpecificOutput", {})["updatedInput"] = {
339
+ "command": cleaned
340
+ }
341
+ return BashValidationResult(
342
+ allowed=True,
343
+ tier=SecurityTier.T0_READ_ONLY,
344
+ reason=reason,
345
+ modified_input={"command": cleaned},
346
+ block_response=hook_response,
347
+ )
348
+
349
+ def validate(
350
+ self,
351
+ command: str,
352
+ is_subagent: bool = False,
353
+ session_id: str = "",
354
+ ) -> BashValidationResult:
355
+ """
356
+ Validate a Bash command through the 5-phase pipeline.
357
+
358
+ Phases:
359
+ 1. UNWRAP - strip shell wrappers, detect obfuscation
360
+ 2. DECOMPOSE - split into operator-linked stages
361
+ 3. CLASSIFY - blocked_commands + cloud_pipe + mutative_verbs
362
+ 4. COMPOSITION - cross-stage pattern checks (stub for T4)
363
+ 5. AGGREGATE - combine results into final verdict
364
+
365
+ Args:
366
+ command: Command string to validate
367
+ is_subagent: True when running in subagent context
368
+ session_id: Session ID for approval scoping
369
+
370
+ Returns:
371
+ BashValidationResult with validation details
372
+ """
373
+ if not command or not command.strip():
374
+ return BashValidationResult(
375
+ allowed=False,
376
+ tier=SecurityTier.T3_BLOCKED,
377
+ reason="Empty command not allowed",
378
+ )
379
+
380
+ command = command.strip()
381
+
382
+ # ================================================================
383
+ # EARLY NORMALIZATION: Strip AI attribution footers before any
384
+ # other processing. This ensures the same normalized command
385
+ # string is used for blocked-command checks, compound parsing,
386
+ # mutative verb detection, pending approval writes, AND pending
387
+ # approval lookups. Without this, write_pending_approval() and
388
+ # find_pending_for_command() could see different strings on the
389
+ # first attempt vs. retry, causing nonce mismatch loops.
390
+ # ================================================================
391
+ command_was_modified = False
392
+ if self._detect_claude_footers(command):
393
+ command = self._strip_claude_footers(command)
394
+ command_was_modified = True
395
+ logger.info("Auto-stripped Claude Code footer from commit command")
396
+
397
+ # ================================================================
398
+ # PHASE 1: UNWRAP
399
+ # Use ShellUnwrapper to detect and strip shell wrapper layers
400
+ # (bash -c, sh -c, env bash -c, etc.). If the wrapper nesting
401
+ # depth exceeds _OBFUSCATION_DEPTH_LIMIT, permanently block
402
+ # the command as obfuscated.
403
+ #
404
+ # After the unwrapper, _detect_indirect_execution() runs as a
405
+ # fallback for patterns the unwrapper does not cover: eval,
406
+ # python -c, node -e, perl -e, ruby -e, process substitution.
407
+ # ================================================================
408
+ unwrap_result = self._unwrapper.unwrap(command)
409
+ if unwrap_result.depth >= _OBFUSCATION_DEPTH_LIMIT:
410
+ return BashValidationResult(
411
+ allowed=False,
412
+ tier=SecurityTier.T3_BLOCKED,
413
+ reason=(
414
+ f"Obfuscated shell nesting detected: {unwrap_result.depth} "
415
+ f"wrapper layers exceeds limit of {_OBFUSCATION_DEPTH_LIMIT}"
416
+ ),
417
+ )
418
+
419
+ indirect_result = self._detect_indirect_execution(command)
420
+ if indirect_result is not None:
421
+ return indirect_result
422
+
423
+ # ================================================================
424
+ # PHASE 2: DECOMPOSE
425
+ # Split the command into operator-linked stages using
426
+ # StageDecomposer. The decomposed result is available for
427
+ # phase 4 (composition rules, T4) and provides operator context
428
+ # that ShellCommandParser.parse() discards.
429
+ # ================================================================
430
+ decomposed = self._decomposer.decompose(command)
431
+
432
+ # ================================================================
433
+ # PHASE 3: CLASSIFY STAGES
434
+ # Apply existing classification logic in priority order:
435
+ # 3a. blocked_commands on full command (exit 2)
436
+ # 3b. blocked_commands on each compound component (exit 2)
437
+ # 3c. Git commit message validation
438
+ # 3d. Smart sanitization (strip nohup, &, redirects)
439
+ # 3e. Cloud pipe/redirect/chain check (corrective deny)
440
+ # 3f. Dispatch to single/compound classification
441
+ # (mutative_verbs, gitops_validator, safe-by-elimination)
442
+ # ================================================================
443
+
444
+ # 3a. Blocked commands check on FULL command (exit 2).
445
+ # This MUST run before any other classifier to ensure permanently
446
+ # blocked commands (kubectl delete namespace, etc.) are caught
447
+ # with a reliable exit 2.
448
+ blocked_result = is_blocked_command(command)
449
+ if blocked_result.is_blocked:
450
+ return BashValidationResult(
451
+ allowed=False,
452
+ tier=SecurityTier.T3_BLOCKED,
453
+ reason=f"Command blocked by security policy: {blocked_result.category}",
454
+ suggestions=[blocked_result.suggestion] if blocked_result.suggestion else [],
455
+ )
456
+
457
+ # 3b. Parse compound commands and check each component against the
458
+ # deny list. Uses ShellCommandParser (not StageDecomposer) for
459
+ # backward compat — the decomposed stages are used in phase 4.
460
+ has_operators = self._has_operators(command)
461
+ parsed_components = None
462
+ if has_operators:
463
+ parsed_components = self.shell_parser.parse(command)
464
+ # Check each component against the deny list.
465
+ # This catches "ls && kubectl delete namespace prod" early.
466
+ for component in parsed_components:
467
+ comp_blocked = is_blocked_command(component.strip())
468
+ if comp_blocked.is_blocked:
469
+ return BashValidationResult(
470
+ allowed=False,
471
+ tier=SecurityTier.T3_BLOCKED,
472
+ reason=f"Command blocked by security policy: {comp_blocked.category}",
473
+ suggestions=[comp_blocked.suggestion] if comp_blocked.suggestion else [],
474
+ )
475
+
476
+ # 3c. Validate git commit messages (on the potentially cleaned command).
477
+ if "git commit" in command and "-m" in command:
478
+ commit_validation = self._validate_commit_message(command)
479
+ if not commit_validation.allowed:
480
+ return commit_validation
481
+
482
+ # 3d. Smart sanitization: strip nohup, trailing &, trailing redirect.
483
+ sanitized = self._try_sanitize_command(command)
484
+ if sanitized is not None:
485
+ if sanitized.allowed:
486
+ return sanitized
487
+ else:
488
+ return sanitized
489
+
490
+ # 3e. Cloud pipe/redirect/chaining check.
491
+ pipe_block = validate_cloud_pipe(command)
492
+ if pipe_block is not None:
493
+ return BashValidationResult(
494
+ allowed=False,
495
+ tier=SecurityTier.T3_BLOCKED,
496
+ reason=pipe_block["hookSpecificOutput"]["permissionDecisionReason"],
497
+ suggestions=[],
498
+ modified_input=None,
499
+ block_response=pipe_block,
500
+ )
501
+
502
+ # ================================================================
503
+ # PHASE 4: CHECK COMPOSITION
504
+ # Cross-stage composition rules detect dangerous pipe patterns:
505
+ # - Exfiltration: sensitive_read | network_write
506
+ # - RCE: network_read | exec_sink
507
+ # - Obfuscated: decode | exec_sink
508
+ # - File-to-exec: file_read | exec_sink (escalate)
509
+ # Only pipe-connected stages are checked; &&/; are independent.
510
+ # ================================================================
511
+ _composition_result = self._phase4_check_composition(decomposed)
512
+ if _composition_result is not None:
513
+ return _composition_result
514
+
515
+ # ================================================================
516
+ # PHASE 5: AGGREGATE
517
+ # 3f. Dispatch to per-stage classifiers (single or compound)
518
+ # and combine into the final BashValidationResult.
519
+ # ================================================================
520
+ if not has_operators:
521
+ result = self._validate_single_command(
522
+ command, is_subagent=is_subagent, session_id=session_id,
523
+ )
524
+ elif parsed_components is not None and len(parsed_components) > 1:
525
+ result = self._validate_compound_command(
526
+ parsed_components, is_subagent=is_subagent, session_id=session_id,
527
+ )
528
+ else:
529
+ result = self._validate_single_command(
530
+ command, is_subagent=is_subagent, session_id=session_id,
531
+ )
532
+
533
+ # Attach cleaned command for hook to emit via updatedInput.
534
+ # Set regardless of result.allowed so the ask path can include it too.
535
+ if command_was_modified:
536
+ result.modified_input = {"command": command}
537
+ # If the result is an "ask" block_response, inject updatedInput
538
+ # so the modification survives the native permission dialog.
539
+ if (
540
+ result.block_response is not None
541
+ and result.block_response.get("hookSpecificOutput", {}).get(
542
+ "permissionDecision"
543
+ ) == "ask"
544
+ ):
545
+ result.block_response["hookSpecificOutput"]["updatedInput"] = {
546
+ "command": command
547
+ }
548
+
549
+ return result
550
+
551
+ def _validate_single_command(
552
+ self,
553
+ command: str,
554
+ is_subagent: bool = False,
555
+ session_id: str = "",
556
+ ) -> BashValidationResult:
557
+ """Validate a single command (no operators).
558
+
559
+ Simplified pipeline:
560
+ 0. Indirect execution detection (for compound command components)
561
+ 1. Mutative verb detection -> block with nonce or allow with grant
562
+ 2. GitOps policy validation (for kubectl/helm/flux)
563
+ 3. Everything else -> SAFE by elimination
564
+
565
+ Args:
566
+ command: The command to validate.
567
+ is_subagent: True when running in subagent context (generates
568
+ approval_id + deny). False for orchestrator (returns ask).
569
+ session_id: Session ID for pending approval scoping.
570
+
571
+ Note: is_blocked_command() is NOT called here because validate()
572
+ already checks the full command AND each compound component against
573
+ the deny list before dispatching to this method.
574
+ """
575
+
576
+ # Indirect execution check for compound command components.
577
+ # When validate() splits "cd /tmp && python3 -c '...'" into parts,
578
+ # the python3 -c component needs the same indirect execution gate
579
+ # that the full command gets in validate().
580
+ indirect_result = self._detect_indirect_execution(command)
581
+ if indirect_result is not None:
582
+ return indirect_result
583
+
584
+ # Mutative verb detection
585
+ result = detect_mutative_command(command)
586
+ if result.is_mutative:
587
+ # Check for an active approval grant before blocking.
588
+ grant = check_approval_grant(command, session_id=session_id)
589
+ if grant is not None:
590
+ if grant.confirmed:
591
+ # Already confirmed and consumed -- should not reach
592
+ # here (single-use). But if it does, allow through.
593
+ logger.info(
594
+ "T3 command allowed via confirmed grant: %s (scope='%s')",
595
+ command[:80], grant.approved_scope,
596
+ )
597
+ return BashValidationResult(
598
+ allowed=True,
599
+ tier=SecurityTier.T3_BLOCKED,
600
+ reason="Grant confirmed",
601
+ )
602
+ else:
603
+ # Grant exists, not yet confirmed -- GAIA approved,
604
+ # let it through. PostToolUse will confirm and consume
605
+ # the grant after successful execution.
606
+ logger.info(
607
+ "T3 command passthrough via active grant: %s (scope='%s')",
608
+ command[:80], grant.approved_scope,
609
+ )
610
+ return BashValidationResult(
611
+ allowed=True,
612
+ tier=SecurityTier.T3_BLOCKED,
613
+ reason="Grant active, pending confirmation",
614
+ )
615
+ else:
616
+ if is_subagent:
617
+ # Subagent context: check for an existing pending
618
+ # approval first (retry scenario). If found, reuse
619
+ # the same nonce to prevent infinite approval_id
620
+ # generation loops while the user reviews.
621
+ existing_nonce = find_pending_for_command(
622
+ session_id or "", command,
623
+ )
624
+ if existing_nonce:
625
+ approval_id = existing_nonce
626
+ logger.info(
627
+ "Reusing pending approval_id=%s for retry: %s",
628
+ approval_id, command[:80],
629
+ )
630
+ reason = (
631
+ f"[T3_BLOCKED] This command requires user approval.\n"
632
+ f"Do NOT retry this command. Report APPROVAL_REQUEST with this approval_id in your json:contract.\n"
633
+ f"Command: {command}\n"
634
+ f"Verb: '{result.verb}' ({result.category})\n"
635
+ f"approval_id: {approval_id}"
636
+ )
637
+ hook_deny = build_hook_permission_response("deny", reason)
638
+ return BashValidationResult(
639
+ allowed=False,
640
+ tier=SecurityTier.T3_BLOCKED,
641
+ reason=f"T3 {result.category.lower()} command: {result.reason}",
642
+ block_response=hook_deny,
643
+ )
644
+ # No existing pending -- generate a new nonce.
645
+ # The ElicitationResult hook will activate the
646
+ # grant when the user approves via AskUserQuestion.
647
+ approval_id = generate_nonce()
648
+ pending_path = write_pending_approval(
649
+ nonce=approval_id,
650
+ command=command,
651
+ danger_verb=result.verb,
652
+ danger_category=result.category,
653
+ session_id=session_id or None,
654
+ cwd=os.getcwd(),
655
+ )
656
+ if pending_path is None:
657
+ # Persistence failure — fall back to ask
658
+ logger.warning(
659
+ "Failed to persist pending approval for subagent; "
660
+ "falling back to ask: %s",
661
+ command[:80],
662
+ )
663
+ reason = build_pending_approval_unavailable_message()
664
+ hook_ask = build_hook_permission_response("ask", reason)
665
+ return BashValidationResult(
666
+ allowed=False,
667
+ tier=SecurityTier.T3_BLOCKED,
668
+ reason="Pending approval persistence failed",
669
+ block_response=hook_ask,
670
+ )
671
+ reason = (
672
+ f"[T3_BLOCKED] This command requires user approval.\n"
673
+ f"Do NOT retry this command. Report APPROVAL_REQUEST with this approval_id in your json:contract.\n"
674
+ f"Command: {command}\n"
675
+ f"Verb: '{result.verb}' ({result.category})\n"
676
+ f"approval_id: {approval_id}"
677
+ )
678
+ hook_deny = build_hook_permission_response("deny", reason)
679
+ return BashValidationResult(
680
+ allowed=False,
681
+ tier=SecurityTier.T3_BLOCKED,
682
+ reason=f"T3 {result.category.lower()} command: {result.reason}",
683
+ block_response=hook_deny,
684
+ )
685
+ else:
686
+ # Orchestrator context: route through native 'ask' dialog.
687
+ # The user sees the native permission prompt and approves
688
+ # directly. No approval_id is generated.
689
+ reason = (
690
+ f"[T3_APPROVAL_REQUIRED] {result.category} operation detected.\n"
691
+ f"Command: {command}\n"
692
+ f"Verb: '{result.verb}' ({result.category})\n"
693
+ f"Reason: {result.reason}"
694
+ )
695
+ hook_ask = build_hook_permission_response("ask", reason)
696
+ return BashValidationResult(
697
+ allowed=False,
698
+ tier=SecurityTier.T3_BLOCKED,
699
+ reason=f"Dangerous {result.category.lower()} command: {result.reason}",
700
+ block_response=hook_ask,
701
+ )
702
+
703
+ # Check GitOps policy for kubectl/helm/flux commands
704
+ if any(keyword in command for keyword in ("kubectl", "helm", "flux")):
705
+ gitops_result = validate_gitops_workflow(command)
706
+ if not gitops_result.allowed:
707
+ return BashValidationResult(
708
+ allowed=False,
709
+ tier=SecurityTier.T3_BLOCKED,
710
+ reason=f"GitOps policy violation: {gitops_result.reason}",
711
+ suggestions=gitops_result.suggestions,
712
+ )
713
+
714
+ # Flag-dependent classification (sed -i, find -exec, tar -x, etc.)
715
+ # This supplements mutative_verbs -- it catches flag-dependent mutations
716
+ # that verb-based detection misses (e.g. "sed" has no mutative verb, but
717
+ # "sed -i" is mutative). Runs after blocked_commands and mutative_verbs
718
+ # to avoid double-classification.
719
+ #
720
+ # Git commands are EXCLUDED from the MUTATIVE path here because
721
+ # detect_mutative_command() already has deliberate git handling. If it
722
+ # chose not to block a git command, that decision should be respected.
723
+ # Git BLOCKED results still fire as a safety net (force push, etc.).
724
+ flag_result = classify_by_flags(command)
725
+ if flag_result is not None:
726
+ if flag_result.outcome == FLAG_BLOCKED:
727
+ return BashValidationResult(
728
+ allowed=False,
729
+ tier=SecurityTier.T3_BLOCKED,
730
+ reason=f"Command blocked by flag classifier: {flag_result.reason}",
731
+ suggestions=[],
732
+ )
733
+ if flag_result.outcome == FLAG_MUTATIVE:
734
+ # Skip git commands -- mutative_verbs already handles them.
735
+ if flag_result.command_family.startswith("git_"):
736
+ pass # Fall through to safe-by-elimination
737
+ else:
738
+ reason = (
739
+ f"[T3_APPROVAL_REQUIRED] Flag-dependent mutation detected.\n"
740
+ f"Command: {command}\n"
741
+ f"Flag: {flag_result.matched_pattern} ({flag_result.command_family})\n"
742
+ f"Reason: {flag_result.reason}"
743
+ )
744
+ hook_ask = build_hook_permission_response("ask", reason)
745
+ return BashValidationResult(
746
+ allowed=False,
747
+ tier=SecurityTier.T3_BLOCKED,
748
+ reason=f"Mutative flag detected: {flag_result.reason}",
749
+ block_response=hook_ask,
750
+ )
751
+
752
+ # Not blocked, not mutative -> SAFE by elimination
753
+ return BashValidationResult(
754
+ allowed=True,
755
+ tier=SecurityTier.T0_READ_ONLY,
756
+ reason="Safe by elimination (not blocked, not mutative)",
757
+ )
758
+
759
+ def _validate_compound_command(
760
+ self,
761
+ components: List[str],
762
+ is_subagent: bool = False,
763
+ session_id: str = "",
764
+ ) -> BashValidationResult:
765
+ """Validate a compound command (multiple components)."""
766
+ logger.info(f"Compound command detected with {len(components)} components")
767
+
768
+ component_results: List[BashValidationResult] = []
769
+ for i, component in enumerate(components, 1):
770
+ result = self._validate_single_command(
771
+ component, is_subagent=is_subagent, session_id=session_id,
772
+ )
773
+
774
+ if not result.allowed:
775
+ return BashValidationResult(
776
+ allowed=False,
777
+ tier=SecurityTier.T3_BLOCKED,
778
+ reason=(
779
+ f"Compound command blocked: component {i}/{len(components)} "
780
+ f"'{component[:50]}' is not allowed\n"
781
+ f"Reason: {result.reason}"
782
+ ),
783
+ suggestions=result.suggestions,
784
+ block_response=result.block_response,
785
+ )
786
+ component_results.append(result)
787
+
788
+ # All components validated -- derive highest tier from results already
789
+ # computed by _validate_single_command (avoids redundant classification).
790
+ tier_order = ["T0", "T1", "T2", "T3"]
791
+ highest_tier = max(
792
+ (r.tier for r in component_results),
793
+ key=lambda t: tier_order.index(t.value),
794
+ )
795
+
796
+ return BashValidationResult(
797
+ allowed=True,
798
+ tier=highest_tier,
799
+ reason=f"All {len(components)} components validated",
800
+ )
801
+
802
+ def _phase4_check_composition(
803
+ self, decomposed: DecomposedCommand,
804
+ ) -> Optional[BashValidationResult]:
805
+ """Check cross-stage composition patterns (Phase 4).
806
+
807
+ Detects dangerous pipe compositions:
808
+ - Exfiltration: sensitive_read | network_write -> permanent block
809
+ - RCE: network_read | exec_sink -> permanent block
810
+ - Obfuscated exec: decode | exec_sink -> permanent block
811
+ - File-to-exec: file_read | exec_sink -> escalate (ask)
812
+
813
+ Args:
814
+ decomposed: Output from StageDecomposer.decompose().
815
+
816
+ Returns:
817
+ BashValidationResult if a composition rule fires, else None.
818
+ """
819
+ if not decomposed.stages or len(decomposed.stages) < 2:
820
+ return None
821
+
822
+ # Check whether any stages are pipe-connected.
823
+ has_pipe = any(s.operator == "|" for s in decomposed.stages)
824
+ if not has_pipe:
825
+ return None
826
+
827
+ # Build classified composition stages and check rules.
828
+ comp_stages = build_composition_stages(decomposed.stages)
829
+ result = check_composition(comp_stages)
830
+
831
+ if result.decision == CompositionDecision.BLOCK:
832
+ return BashValidationResult(
833
+ allowed=False,
834
+ tier=SecurityTier.T3_BLOCKED,
835
+ reason=f"Dangerous pipe composition blocked: {result.reason}",
836
+ )
837
+
838
+ if result.decision == CompositionDecision.ESCALATE:
839
+ reason = (
840
+ f"[T3_APPROVAL_REQUIRED] Potentially dangerous pipe composition.\n"
841
+ f"Pattern: {result.pattern}\n"
842
+ f"Reason: {result.reason}"
843
+ )
844
+ hook_ask = build_hook_permission_response("ask", reason)
845
+ return BashValidationResult(
846
+ allowed=False,
847
+ tier=SecurityTier.T3_BLOCKED,
848
+ reason=f"Pipe composition requires approval: {result.reason}",
849
+ block_response=hook_ask,
850
+ )
851
+
852
+ # No composition rule fired — continue to Phase 5.
853
+ return None
854
+
855
+ def _detect_claude_footers(self, command: str) -> bool:
856
+ """Detect Claude Code attribution footers in command."""
857
+ for pattern in FORBIDDEN_FOOTER_PATTERNS:
858
+ if re.search(pattern, command, re.IGNORECASE):
859
+ return True
860
+ return False
861
+
862
+ def _strip_claude_footers(self, command: str) -> str:
863
+ """
864
+ Strip Claude Code attribution footers from a command.
865
+
866
+ Removes full lines matching forbidden footer patterns.
867
+ Works on raw command string regardless of quoting/HEREDOC format.
868
+ Preserves trailing quote/paren characters that close the commit
869
+ message (e.g., the closing " in -m "...footer").
870
+
871
+ Args:
872
+ command: Raw command string
873
+
874
+ Returns:
875
+ Command with footer lines removed
876
+ """
877
+ # Remove full lines that contain AI attribution patterns.
878
+ # Each pattern matches the newline + footer content, then uses a
879
+ # lookahead to stop before any trailing quote/paren/bracket
880
+ # sequence that closes the command structure. The captured group
881
+ # is replaced with empty string, leaving the closing chars intact.
882
+ footer_line_patterns = [
883
+ r'\n\s*Co-[Aa]uthored-[Bb]y:\s+(?:Claude|GitHub Copilot|aider|Windsurf|Cursor|Codex|Gemini)[^\n]*?(?=["\')\]]*(?:\n|$))',
884
+ r'\n\s*Generated with\s+\[?Claude Code\]?[^\n]*?(?=["\')\]]*(?:\n|$))',
885
+ r'\n\s*🤖\s*Generated with[^\n]*?(?=["\')\]]*(?:\n|$))',
886
+ ]
887
+ for pattern in footer_line_patterns:
888
+ command = re.sub(pattern, '', command, flags=re.IGNORECASE)
889
+
890
+ # Clean up trailing whitespace inside quotes/heredoc
891
+ # Collapse 3+ consecutive newlines to 2
892
+ command = re.sub(r'\n{3,}', '\n\n', command)
893
+
894
+ return command
895
+
896
+ def _validate_commit_message(self, command: str) -> BashValidationResult:
897
+ """
898
+ Validate git commit message using commit_validator.
899
+
900
+ Args:
901
+ command: Git commit command to validate
902
+
903
+ Returns:
904
+ BashValidationResult with validation status
905
+ """
906
+ # Extract commit message from command
907
+ # Handles both: git commit -m "message" and git commit -m "$(cat <<'EOF'...)"
908
+ message = self._extract_commit_message(command)
909
+
910
+ if not message:
911
+ # Could not extract message - let it pass, git will handle it
912
+ return BashValidationResult(
913
+ allowed=True,
914
+ tier=SecurityTier.T2_DRY_RUN,
915
+ reason="Could not extract commit message for validation"
916
+ )
917
+
918
+ # Import validator (lazy import to avoid startup cost)
919
+ try:
920
+ import sys
921
+ from pathlib import Path
922
+
923
+ # Import from sibling module (hooks/modules/validation)
924
+ from ..validation.commit_validator import validate_commit_message
925
+
926
+ # Validate message
927
+ validation = validate_commit_message(message)
928
+
929
+ if not validation.valid:
930
+ # Build suggestions from errors
931
+ suggestions = []
932
+ for error in validation.errors:
933
+ suggestions.append(f"{error['type']}: {error['fix']}")
934
+
935
+ return BashValidationResult(
936
+ allowed=False,
937
+ tier=SecurityTier.T3_BLOCKED,
938
+ reason=f"Commit message validation failed: {validation.errors[0]['message']}",
939
+ suggestions=suggestions[:3] # Limit to 3 suggestions
940
+ )
941
+
942
+ return BashValidationResult(
943
+ allowed=True,
944
+ tier=SecurityTier.T2_DRY_RUN,
945
+ reason="Commit message validated successfully"
946
+ )
947
+
948
+ except Exception as e:
949
+ logger.warning(f"Failed to validate commit message: {e}")
950
+ # If validation fails, allow the command (don't block on validator failure)
951
+ return BashValidationResult(
952
+ allowed=True,
953
+ tier=SecurityTier.T2_DRY_RUN,
954
+ reason=f"Commit validation skipped (validator error: {e})"
955
+ )
956
+
957
+ def _extract_commit_message(self, command: str) -> Optional[str]:
958
+ """
959
+ Extract commit message from git commit command.
960
+
961
+ Handles formats:
962
+ - git commit -m "message"
963
+ - git commit -m 'message'
964
+ - git commit -m "$(cat <<'EOF'\nmessage\nEOF\n)"
965
+ - git commit -m "$(cat <<EOF\nmessage\nEOF\n)"
966
+
967
+ Returns:
968
+ Extracted message or None if cannot extract
969
+ """
970
+ # Level 1: HEREDOC pattern (most common in Claude Code)
971
+ # Handles: <<'EOF', <<EOF, <<"EOF" with flexible whitespace
972
+ if "<<" in command:
973
+ heredoc_match = re.search(
974
+ r"<<['\"]?EOF['\"]?\s*\n(.*?)\n\s*EOF",
975
+ command, re.DOTALL
976
+ )
977
+ if heredoc_match:
978
+ return heredoc_match.group(1).strip()
979
+
980
+ # Level 2: Simple -m "message" or -m 'message' (non-heredoc)
981
+ match = re.search(r'-m\s+(["\'])(.*?)\1', command, re.DOTALL)
982
+ if match:
983
+ msg = match.group(2)
984
+ # Skip if it's a $(cat... wrapper — heredoc parse failed above
985
+ if msg.lstrip().startswith("$(cat"):
986
+ return None
987
+ return msg.strip()
988
+
989
+ return None
990
+
991
+ def validate_bash_command(
992
+ command: str,
993
+ is_subagent: bool = False,
994
+ session_id: str = "",
995
+ ) -> BashValidationResult:
996
+ """
997
+ Validate a Bash command (convenience function).
998
+
999
+ Args:
1000
+ command: Command to validate
1001
+ is_subagent: True when running in subagent context
1002
+ session_id: Session ID for approval scoping
1003
+
1004
+ Returns:
1005
+ BashValidationResult
1006
+ """
1007
+ validator = BashValidator()
1008
+ return validator.validate(command, is_subagent=is_subagent, session_id=session_id)