@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,647 @@
1
+ """
2
+ Contract validation for agent output: structural checks, evidence parsing,
3
+ command extraction, PLAN_STATUS parsing, and exit code derivation.
4
+
5
+ Only the ``json:contract`` fenced-block format is supported. Legacy
6
+ HTML-comment blocks (``<!-- AGENT_STATUS -->``, etc.) are **not** parsed here.
7
+
8
+ Provides:
9
+ - parse_contract(): Extract structured dict from json:contract fenced block
10
+ - validate(): Check agent output against contract requirements -> ValidationResult
11
+ - extract_commands_from_evidence(): Parse COMMANDS_RUN field
12
+ - requires_consolidation_report(): Check if consolidation is needed
13
+ - extract_plan_status_from_output(): Extract PLAN_STATUS string
14
+ - extract_exit_code_from_output(): Derive exit code from PLAN_STATUS
15
+ """
16
+
17
+ import json
18
+ import logging
19
+ import re
20
+ from dataclasses import dataclass
21
+ from typing import Any, Dict, List, Optional
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ _NOT_RUN_INDICATORS = re.compile(
26
+ r"\b(not\s+run|not\s+executed|skipped|n/a)\b",
27
+ re.IGNORECASE,
28
+ )
29
+
30
+ _LITERAL_NONE_COMMANDS = {"none", "not run", "not executed", "n/a", "skipped"}
31
+
32
+ # Required evidence fields
33
+ _EVIDENCE_REQUIRED_FIELDS = [
34
+ "PATTERNS_CHECKED", "FILES_CHECKED", "COMMANDS_RUN", "KEY_OUTPUTS",
35
+ "VERBATIM_OUTPUTS", "CROSS_LAYER_IMPACTS", "OPEN_GAPS",
36
+ ]
37
+
38
+ # Required consolidation fields
39
+ _CONSOLIDATION_REQUIRED_FIELDS = [
40
+ "OWNERSHIP_ASSESSMENT", "CONFIRMED_FINDINGS", "SUSPECTED_FINDINGS",
41
+ "CONFLICTS", "OPEN_GAPS", "NEXT_BEST_AGENT",
42
+ ]
43
+
44
+
45
+ @dataclass
46
+ class ValidationResult:
47
+ """Result of contract validation.
48
+
49
+ Attributes:
50
+ is_valid: True if all required contract blocks are present and complete.
51
+ missing: List of missing block/field names.
52
+ error_message: Descriptive error for stderr output when is_valid is False.
53
+ """
54
+ is_valid: bool
55
+ missing: List[str]
56
+ error_message: str
57
+
58
+
59
+ # ============================================================================
60
+ # JSON contract parser
61
+ # ============================================================================
62
+
63
+ def parse_contract(agent_output: str) -> Optional[dict]:
64
+ """Extract structured contract dict from a ``json:contract`` fenced block.
65
+
66
+ Searches for the first occurrence of a fenced code block tagged
67
+ ``json:contract`` and attempts to parse its contents as JSON.
68
+
69
+ Args:
70
+ agent_output: Complete output from agent execution.
71
+
72
+ Returns:
73
+ Parsed dict if a valid json:contract block is found, None otherwise.
74
+ """
75
+ m = re.search(r'```json:contract\s*\n(.*?)```', agent_output, re.DOTALL)
76
+ if not m:
77
+ return None
78
+ try:
79
+ return json.loads(m.group(1))
80
+ except json.JSONDecodeError:
81
+ return None
82
+
83
+
84
+ # ============================================================================
85
+ # JSON contract validation helpers
86
+ # ============================================================================
87
+
88
+ def _validate_from_json_contract(contract: dict, task_info: Dict[str, Any]) -> ValidationResult:
89
+ """Validate agent output using the parsed JSON contract dict.
90
+
91
+ Checks that the contract dict contains the required keys:
92
+ - agent_status with plan_status and agent_id
93
+ - evidence_report with required fields (when plan_status requires it)
94
+ - consolidation_report (when multi-surface task requires it)
95
+
96
+ Args:
97
+ contract: Parsed dict from parse_contract().
98
+ task_info: Task metadata including injected_context for multi-surface detection.
99
+
100
+ Returns:
101
+ ValidationResult with is_valid, missing fields list, and error_message.
102
+ """
103
+ all_missing: List[str] = []
104
+
105
+ # 1. Check agent_status
106
+ agent_status = contract.get("agent_status")
107
+ if not agent_status or not isinstance(agent_status, dict):
108
+ all_missing.extend(["AGENT_STATUS", "PLAN_STATUS", "AGENT_ID"])
109
+ else:
110
+ if not agent_status.get("plan_status"):
111
+ all_missing.append("PLAN_STATUS")
112
+ if not agent_status.get("agent_id"):
113
+ all_missing.append("AGENT_ID")
114
+
115
+ # Determine plan_status for evidence check
116
+ plan_status = ""
117
+ if agent_status and isinstance(agent_status, dict):
118
+ plan_status = str(agent_status.get("plan_status", "")).upper()
119
+
120
+ statuses_requiring_evidence = {
121
+ "IN_PROGRESS", "REVIEW",
122
+ "COMPLETE", "BLOCKED", "NEEDS_INPUT",
123
+ }
124
+
125
+ if plan_status in statuses_requiring_evidence:
126
+ # 2. Check evidence_report
127
+ evidence = contract.get("evidence_report")
128
+ if not evidence or not isinstance(evidence, dict):
129
+ all_missing.append("EVIDENCE_REPORT")
130
+ else:
131
+ for field in _EVIDENCE_REQUIRED_FIELDS:
132
+ # Accept both lower-case keys (JSON style) and upper-case (legacy)
133
+ key_lower = field.lower()
134
+ if not evidence.get(key_lower) and not evidence.get(field):
135
+ all_missing.append(field)
136
+
137
+ # 3. Check consolidation_report (only when required)
138
+ if requires_consolidation_report(task_info):
139
+ consolidation = contract.get("consolidation_report")
140
+ if not consolidation or not isinstance(consolidation, dict):
141
+ all_missing.append("CONSOLIDATION_REPORT")
142
+ else:
143
+ for field in _CONSOLIDATION_REQUIRED_FIELDS:
144
+ key_lower = field.lower()
145
+ if not consolidation.get(key_lower) and not consolidation.get(field):
146
+ all_missing.append(field)
147
+
148
+ if all_missing:
149
+ fields_str = ", ".join(all_missing)
150
+ error_message = (
151
+ f"Contract incomplete. Missing: {fields_str}.\n"
152
+ f"\n"
153
+ f"Repair: reissue your response ending with a json:contract block:\n"
154
+ f"\n"
155
+ f"```json:contract\n"
156
+ f'{{\n'
157
+ f' "agent_status": {{\n'
158
+ f' "plan_status": "<STATUS>",\n'
159
+ f' "agent_id": "<your-id>",\n'
160
+ f' "pending_steps": [],\n'
161
+ f' "next_action": "<done or next step>"\n'
162
+ f" }},\n"
163
+ f' "evidence_report": {{\n'
164
+ f' "patterns_checked": [],\n'
165
+ f' "files_checked": [],\n'
166
+ f' "commands_run": [],\n'
167
+ f' "key_outputs": [],\n'
168
+ f' "verbatim_outputs": [],\n'
169
+ f' "cross_layer_impacts": [],\n'
170
+ f' "open_gaps": []\n'
171
+ f" }},\n"
172
+ f' "consolidation_report": null\n'
173
+ f"}}\n"
174
+ f"```\n"
175
+ f"\n"
176
+ f"Required fields: agent_status (plan_status, agent_id, pending_steps, next_action), evidence_report\n"
177
+ f"Evidence required fields: patterns_checked, files_checked, commands_run, key_outputs, verbatim_outputs, cross_layer_impacts, open_gaps"
178
+ )
179
+ return ValidationResult(
180
+ is_valid=False,
181
+ missing=all_missing,
182
+ error_message=error_message,
183
+ )
184
+
185
+ return ValidationResult(is_valid=True, missing=[], error_message="")
186
+
187
+
188
+ # ============================================================================
189
+ # Main validation entry point
190
+ # ============================================================================
191
+
192
+ def validate(agent_output: str, task_info: Dict[str, Any]) -> ValidationResult:
193
+ """Validate agent output against contract requirements.
194
+
195
+ Only the ``json:contract`` fenced-block format is supported.
196
+
197
+ Checks:
198
+ 1. AGENT_STATUS block with plan_status and agent_id
199
+ 2. EVIDENCE_REPORT with required fields (when plan_status requires it)
200
+ 3. CONSOLIDATION_REPORT (when multi-surface task requires it)
201
+
202
+ Args:
203
+ agent_output: Complete output from agent execution.
204
+ task_info: Task metadata including injected_context for multi-surface detection.
205
+
206
+ Returns:
207
+ ValidationResult with is_valid, missing fields list, and error_message.
208
+ """
209
+ contract = parse_contract(agent_output)
210
+ if contract is not None:
211
+ return _validate_from_json_contract(contract, task_info)
212
+
213
+ # No json:contract block found -- report everything as missing.
214
+ all_missing = ["AGENT_STATUS", "PLAN_STATUS", "AGENT_ID"]
215
+ fields_str = ", ".join(all_missing)
216
+ error_message = (
217
+ f"Contract incomplete. Missing: {fields_str}. "
218
+ f"No json:contract fenced block found.\n"
219
+ f"\n"
220
+ f"Repair: your response MUST end with a json:contract block:\n"
221
+ f"\n"
222
+ f"```json:contract\n"
223
+ f'{{\n'
224
+ f' "agent_status": {{\n'
225
+ f' "plan_status": "<STATUS>",\n'
226
+ f' "agent_id": "<your-id>",\n'
227
+ f' "pending_steps": [],\n'
228
+ f' "next_action": "<done or next step>"\n'
229
+ f" }},\n"
230
+ f' "evidence_report": {{\n'
231
+ f' "patterns_checked": [],\n'
232
+ f' "files_checked": [],\n'
233
+ f' "commands_run": [],\n'
234
+ f' "key_outputs": [],\n'
235
+ f' "verbatim_outputs": [],\n'
236
+ f' "cross_layer_impacts": [],\n'
237
+ f' "open_gaps": []\n'
238
+ f" }},\n"
239
+ f' "consolidation_report": null\n'
240
+ f"}}\n"
241
+ f"```\n"
242
+ f"\n"
243
+ f"Required fields: agent_status (plan_status, agent_id, pending_steps, next_action), evidence_report\n"
244
+ f"Evidence required fields: patterns_checked, files_checked, commands_run, key_outputs, verbatim_outputs, cross_layer_impacts, open_gaps"
245
+ )
246
+ return ValidationResult(
247
+ is_valid=False,
248
+ missing=all_missing,
249
+ error_message=error_message,
250
+ )
251
+
252
+
253
+ # ============================================================================
254
+ # Functions absorbed from evidence_parser.py (backward compatible)
255
+ # ============================================================================
256
+
257
+ def extract_commands_from_evidence(agent_output: str) -> List[str]:
258
+ """Extract command strings from the EVIDENCE_REPORT COMMANDS_RUN field.
259
+
260
+ Only the ``json:contract`` fenced-block format is supported.
261
+ Extracts from ``evidence_report.commands_run`` list entries.
262
+
263
+ Commands whose result indicates they were NOT actually run (e.g. "not run",
264
+ "skipped", "n/a", "not executed") are excluded from the returned list.
265
+
266
+ Returns a list of command strings (without surrounding backticks).
267
+ """
268
+ contract = parse_contract(agent_output)
269
+ if contract is None:
270
+ return []
271
+
272
+ evidence = contract.get("evidence_report", {}) or {}
273
+ commands_run = evidence.get("commands_run", [])
274
+ if not isinstance(commands_run, list):
275
+ return []
276
+
277
+ commands: List[str] = []
278
+ for entry in commands_run:
279
+ if isinstance(entry, dict):
280
+ cmd = entry.get("command", entry.get("cmd", ""))
281
+ elif isinstance(entry, str):
282
+ cmd = entry
283
+ else:
284
+ continue
285
+ if cmd and cmd.lower() not in _LITERAL_NONE_COMMANDS:
286
+ if not _NOT_RUN_INDICATORS.search(cmd):
287
+ commands.append(cmd)
288
+ return commands
289
+
290
+
291
+ def requires_consolidation_report(task_info: Dict[str, Any]) -> bool:
292
+ """Determine whether runtime should require a CONSOLIDATION_REPORT block.
293
+
294
+ Checks injected_context for investigation_brief.consolidation_required,
295
+ investigation_brief.cross_check_required, or surface_routing.multi_surface.
296
+
297
+ Falls back to reading from the transcript if injected_context was not
298
+ pre-extracted.
299
+ """
300
+ payload = task_info.get("injected_context") or {}
301
+ if not payload:
302
+ # Fallback: read from transcript if injected_context was not pre-extracted
303
+ from .transcript_reader import extract_injected_context_payload_from_transcript
304
+ payload = extract_injected_context_payload_from_transcript(
305
+ task_info.get("agent_transcript_path", "")
306
+ )
307
+ if not payload:
308
+ return False
309
+
310
+ investigation_brief = payload.get("investigation_brief", {}) or {}
311
+ surface_routing = payload.get("surface_routing", {}) or {}
312
+ return bool(
313
+ investigation_brief.get("consolidation_required")
314
+ or investigation_brief.get("cross_check_required")
315
+ or surface_routing.get("multi_surface")
316
+ )
317
+
318
+
319
+ def extract_plan_status_from_output(agent_output: str) -> str:
320
+ """Extract the PLAN_STATUS string from agent output.
321
+
322
+ Only the ``json:contract`` fenced-block format is supported.
323
+
324
+ Returns the raw status string (e.g. "COMPLETE", "BLOCKED", "NEEDS_INPUT")
325
+ or empty string if not found.
326
+ """
327
+ contract = parse_contract(agent_output)
328
+ if contract is None:
329
+ return ""
330
+
331
+ agent_status = contract.get("agent_status", {}) or {}
332
+ plan_status = agent_status.get("plan_status", "")
333
+ if plan_status:
334
+ return str(plan_status).upper().rstrip(".,;")
335
+ return ""
336
+
337
+
338
+ def extract_exit_code_from_output(agent_output: str) -> int:
339
+ """Derive exit code from the LAST AGENT_STATUS block in agent output.
340
+
341
+ Looks for PLAN_STATUS in the final assistant message. If the status
342
+ contains COMPLETE -> 0, BLOCKED or ERROR -> 1. Falls back to 0 when
343
+ no AGENT_STATUS is found (optimistic default).
344
+ """
345
+ status_value = extract_plan_status_from_output(agent_output)
346
+ if status_value:
347
+ if "COMPLETE" in status_value:
348
+ return 0
349
+ if "BLOCKED" in status_value or "ERROR" in status_value:
350
+ return 1
351
+ return 0
352
+
353
+
354
+ # ============================================================================
355
+ # Context-usage anomaly detection
356
+ # ============================================================================
357
+
358
+ # Reuse the anchor extraction regex from anchor_tracker for consistency
359
+ _ANCHOR_FIELDS_RE = re.compile(
360
+ r"(path|name|cluster|project|region|namespace|service|image|"
361
+ r"base_path|config_path|module_path|repository|bucket|sa$|"
362
+ r"service_account|pod_name|terragrunt_path)",
363
+ re.IGNORECASE,
364
+ )
365
+
366
+ _MIN_ANCHOR_LEN = 4
367
+
368
+
369
+ def _extract_context_anchors(project_knowledge: Dict[str, Any]) -> set:
370
+ """Extract anchor strings (paths, names, IDs) from project_knowledge sections.
371
+
372
+ Walks the project_knowledge dict and collects string values from fields
373
+ whose names match anchor-worthy patterns (paths, service names, clusters, etc.).
374
+
375
+ Args:
376
+ project_knowledge: The project_knowledge dict from the injected context.
377
+
378
+ Returns:
379
+ Set of anchor strings.
380
+ """
381
+ anchors: set = set()
382
+
383
+ def _walk(obj: Any, depth: int = 0) -> None:
384
+ if depth > 10:
385
+ return
386
+ if isinstance(obj, dict):
387
+ for key, value in obj.items():
388
+ if isinstance(value, str) and value and _ANCHOR_FIELDS_RE.search(key):
389
+ clean = value.lstrip("./")
390
+ if len(clean) >= _MIN_ANCHOR_LEN:
391
+ anchors.add(clean)
392
+ elif isinstance(value, (dict, list)):
393
+ _walk(value, depth + 1)
394
+ elif isinstance(obj, list):
395
+ for item in obj:
396
+ _walk(item, depth + 1)
397
+
398
+ _walk(project_knowledge)
399
+ return anchors
400
+
401
+
402
+ def check_context_usage(
403
+ project_knowledge: Dict[str, Any],
404
+ evidence_report: Dict[str, Any],
405
+ ) -> Dict[str, Any]:
406
+ """Soft check: detect when an agent ignores injected project context.
407
+
408
+ Extracts anchors from project_knowledge and checks whether ANY of them
409
+ appear in the agent's evidence_report (files_checked, patterns_checked,
410
+ commands_run). If zero overlap, flags ``context_ignored: true``.
411
+
412
+ This is a soft check -- it never fails validation, only adds a flag.
413
+
414
+ Args:
415
+ project_knowledge: The ``project_knowledge`` dict from injected context.
416
+ evidence_report: The ``evidence_report`` dict from the agent's json:contract.
417
+
418
+ Returns:
419
+ Dict with ``context_ignored`` (bool), ``anchors_found`` (int),
420
+ ``anchors_in_evidence`` (int), and ``overlap`` (list of matched anchors).
421
+ """
422
+ if not project_knowledge or not evidence_report:
423
+ return {
424
+ "context_ignored": False,
425
+ "anchors_found": 0,
426
+ "anchors_in_evidence": 0,
427
+ "overlap": [],
428
+ }
429
+
430
+ anchors = _extract_context_anchors(project_knowledge)
431
+ if not anchors:
432
+ return {
433
+ "context_ignored": False,
434
+ "anchors_found": 0,
435
+ "anchors_in_evidence": 0,
436
+ "overlap": [],
437
+ }
438
+
439
+ # Build a single searchable string from evidence fields
440
+ evidence_parts: List[str] = []
441
+
442
+ for field in ("files_checked", "patterns_checked", "commands_run"):
443
+ entries = evidence_report.get(field, [])
444
+ if isinstance(entries, list):
445
+ for entry in entries:
446
+ if isinstance(entry, str):
447
+ evidence_parts.append(entry)
448
+ elif isinstance(entry, dict):
449
+ # commands_run may be dicts with "command" or "cmd" keys
450
+ evidence_parts.append(
451
+ entry.get("command", entry.get("cmd", str(entry)))
452
+ )
453
+
454
+ evidence_text = " ".join(evidence_parts)
455
+
456
+ matched: List[str] = []
457
+ for anchor in anchors:
458
+ if anchor in evidence_text:
459
+ matched.append(anchor)
460
+
461
+ return {
462
+ "context_ignored": len(matched) == 0,
463
+ "anchors_found": len(anchors),
464
+ "anchors_in_evidence": len(matched),
465
+ "overlap": sorted(matched),
466
+ }
467
+
468
+
469
+ # ============================================================================
470
+ # Cross-field validation: verbatim_outputs consistency (Option D)
471
+ # ============================================================================
472
+
473
+ _VERBATIM_PLACEHOLDER_PATTERNS = re.compile(
474
+ r"^(N/?A|none|no\s+output|no\s+output\s+captured|not\s+applicable|"
475
+ r"no\s+commands?\s+run|no\s+verbatim\s+output|n/a|\[\]|-|"
476
+ r"no\s+output\s+to\s+capture|not\s+available)\.?$",
477
+ re.IGNORECASE,
478
+ )
479
+
480
+
481
+ def _is_real_command(entry: str) -> bool:
482
+ """Return True if the commands_run entry represents a real executed command."""
483
+ if not entry or not entry.strip():
484
+ return False
485
+ normalized = entry.strip().lower()
486
+ if normalized in _LITERAL_NONE_COMMANDS:
487
+ return False
488
+ if _NOT_RUN_INDICATORS.search(normalized):
489
+ return False
490
+ return True
491
+
492
+
493
+ def _is_placeholder_output(entry: str) -> bool:
494
+ """Return True if the verbatim_outputs entry is a placeholder, not real output."""
495
+ if not entry or not entry.strip():
496
+ return True
497
+ return bool(_VERBATIM_PLACEHOLDER_PATTERNS.match(entry.strip()))
498
+
499
+
500
+ def validate_verbatim_outputs_consistency(
501
+ parsed_contract: Optional[dict],
502
+ ) -> Optional[Dict[str, Any]]:
503
+ """Cross-field validation: commands_run vs verbatim_outputs.
504
+
505
+ If commands_run has 1+ real entries, verbatim_outputs must have at least 1
506
+ entry that is NOT a placeholder. Returns an anomaly dict if the check fails,
507
+ None if it passes or does not apply.
508
+
509
+ This is advisory only -- it should be logged but never block.
510
+ """
511
+ if parsed_contract is None:
512
+ return None
513
+
514
+ evidence = parsed_contract.get("evidence_report")
515
+ if not evidence or not isinstance(evidence, dict):
516
+ return None
517
+
518
+ commands_run = evidence.get("commands_run", [])
519
+ if not isinstance(commands_run, list):
520
+ return None
521
+
522
+ # Count real commands
523
+ real_commands = []
524
+ for entry in commands_run:
525
+ if isinstance(entry, dict):
526
+ cmd = entry.get("command", entry.get("cmd", ""))
527
+ elif isinstance(entry, str):
528
+ cmd = entry
529
+ else:
530
+ continue
531
+ if _is_real_command(cmd):
532
+ real_commands.append(cmd)
533
+
534
+ if not real_commands:
535
+ return None # No real commands -- check does not apply
536
+
537
+ # Check verbatim_outputs for at least 1 non-placeholder entry
538
+ verbatim_outputs = evidence.get("verbatim_outputs", [])
539
+ if not isinstance(verbatim_outputs, list):
540
+ verbatim_outputs = []
541
+
542
+ has_real_output = False
543
+ for entry in verbatim_outputs:
544
+ text = ""
545
+ if isinstance(entry, str):
546
+ text = entry
547
+ elif isinstance(entry, dict):
548
+ text = entry.get("output", entry.get("content", str(entry)))
549
+ if text and not _is_placeholder_output(text):
550
+ has_real_output = True
551
+ break
552
+
553
+ if has_real_output:
554
+ return None # Passes -- real commands have backing output
555
+
556
+ return {
557
+ "type": "verbatim_outputs_missing",
558
+ "severity": "warning",
559
+ "message": (
560
+ f"Agent ran {len(real_commands)} command(s) but verbatim_outputs "
561
+ f"contains no real output (only placeholders or empty). "
562
+ f"Commands: {', '.join(c[:60] for c in real_commands[:3])}"
563
+ ),
564
+ }
565
+
566
+
567
+ # ============================================================================
568
+ # False pending-approval detection
569
+ # ============================================================================
570
+
571
+
572
+ # ============================================================================
573
+ # Approval request validation
574
+ # ============================================================================
575
+
576
+ _APPROVAL_STATUSES = {"REVIEW"}
577
+
578
+ _APPROVAL_REQUIRED_FIELDS = [
579
+ "operation", "exact_content", "scope", "risk_level", "rollback", "verification",
580
+ ]
581
+
582
+ _VALID_RISK_LEVELS = {"LOW", "MEDIUM", "HIGH", "CRITICAL"}
583
+
584
+ _NONCE_HEX_RE = re.compile(r"^[a-f0-9]{32}$")
585
+
586
+
587
+ def validate_approval_request(
588
+ contract: dict,
589
+ plan_status: str,
590
+ ) -> Optional[Dict[str, Any]]:
591
+ """Validate the approval_request block when plan_status is REVIEW.
592
+
593
+ Advisory only -- returns an anomaly dict if validation fails, None if OK
594
+ or if the check does not apply.
595
+
596
+ Args:
597
+ contract: Parsed dict from parse_contract().
598
+ plan_status: The agent's reported plan_status string (already uppercased).
599
+
600
+ Returns:
601
+ An anomaly dict (severity: info or warning) when the check triggers, None otherwise.
602
+ """
603
+ if plan_status.upper() not in _APPROVAL_STATUSES:
604
+ return None
605
+
606
+ approval_req = contract.get("approval_request")
607
+ if not approval_req or not isinstance(approval_req, dict):
608
+ return {
609
+ "type": "approval_request_missing",
610
+ "severity": "info",
611
+ "detail": (
612
+ f"Agent returned {plan_status} without an approval_request block. "
613
+ f"Expected fields: {', '.join(_APPROVAL_REQUIRED_FIELDS)}"
614
+ ),
615
+ }
616
+
617
+ missing_fields: List[str] = []
618
+ for field in _APPROVAL_REQUIRED_FIELDS:
619
+ if not approval_req.get(field):
620
+ missing_fields.append(field)
621
+
622
+ # Validate risk_level value if present
623
+ risk = str(approval_req.get("risk_level", "")).upper()
624
+ invalid_risk = risk and risk not in _VALID_RISK_LEVELS
625
+
626
+ nonce_issue = None
627
+
628
+ issues: List[str] = []
629
+ if missing_fields:
630
+ issues.append(f"missing fields: {', '.join(missing_fields)}")
631
+ if invalid_risk:
632
+ issues.append(f"invalid risk_level: {risk}")
633
+ if nonce_issue:
634
+ issues.append(nonce_issue)
635
+
636
+ if not issues:
637
+ return None
638
+
639
+ return {
640
+ "type": "approval_request_incomplete",
641
+ "severity": "warning",
642
+ "detail": (
643
+ f"approval_request block for {plan_status} has issues: "
644
+ f"{'; '.join(issues)}"
645
+ ),
646
+ "missing_fields": missing_fields,
647
+ }