@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,269 @@
1
+ """
2
+ Unit tests for context combining logic (T026).
3
+
4
+ Tests scanner-owned section replacement, agent-enriched section preservation,
5
+ mixed section sub-key merge, unknown section preservation, v1-to-v2 upgrade,
6
+ and idempotency.
7
+ """
8
+
9
+ import copy
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Any, Dict
13
+
14
+ import pytest
15
+
16
+ from tools.scan.merge import merge_context
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Helpers
21
+ # ---------------------------------------------------------------------------
22
+
23
+
24
+ def _section_owners() -> Dict[str, str]:
25
+ """Return a realistic section_owners map from ScannerRegistry."""
26
+ return {
27
+ "project_identity": "stack",
28
+ "stack": "stack",
29
+ "git": "git",
30
+ "infrastructure": "infrastructure",
31
+ "orchestration": "orchestration",
32
+ "environment.tools": "tools",
33
+ "environment.tool_preferences": "tools",
34
+ "environment.runtimes": "environment",
35
+ "environment.os": "environment",
36
+ "environment.env_files": "environment",
37
+ }
38
+
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Test data helpers
42
+ # ---------------------------------------------------------------------------
43
+
44
+
45
+ def _make_existing_context() -> Dict[str, Any]:
46
+ """Create a realistic existing project-context with scanner + agent data."""
47
+ return {
48
+ "metadata": {
49
+ "version": "2.0",
50
+ "last_updated": "2026-01-01T00:00:00Z",
51
+ "project_name": "test-project",
52
+ "scan_config": {
53
+ "staleness_hours": 24,
54
+ "last_scan": "2026-01-01T00:00:00Z",
55
+ "scanner_version": "0.1.0",
56
+ },
57
+ },
58
+ "project_identity": {
59
+ "_source": "scanner:stack",
60
+ "name": "old-name",
61
+ "type": "application",
62
+ },
63
+ "stack": {
64
+ "_source": "scanner:stack",
65
+ "languages": [{"name": "python", "manifest": "pyproject.toml", "primary": True}],
66
+ "frameworks": [],
67
+ "build_tools": [],
68
+ },
69
+ "git": {
70
+ "_source": "scanner:git",
71
+ "platform": "github",
72
+ "remotes": [],
73
+ "default_branch": "main",
74
+ },
75
+ "environment": {
76
+ "_source": "scanner:environment",
77
+ "os": {"platform": "linux", "architecture": "x64"},
78
+ "runtimes": [{"name": "python3", "version": "3.11.0"}],
79
+ "env_files": [],
80
+ "tools": [{"name": "git", "path": "/usr/bin/git"}],
81
+ "tool_preferences": {"file_viewer": "bat"},
82
+ },
83
+ "operational_guidelines": {
84
+ "_source": "agent:devops-developer",
85
+ "deployment_strategy": "blue-green",
86
+ "rollback_procedure": "manual",
87
+ },
88
+ "my_custom_notes": {
89
+ "author": "user",
90
+ "notes": "User-maintained section",
91
+ },
92
+ }
93
+
94
+
95
+ def _make_scan_results() -> Dict[str, Any]:
96
+ """Create scan results from a new scan run."""
97
+ return {
98
+ "project_identity": {
99
+ "_source": "scanner:stack",
100
+ "name": "new-name",
101
+ "type": "monorepo",
102
+ "description": "Updated description",
103
+ },
104
+ "stack": {
105
+ "_source": "scanner:stack",
106
+ "languages": [
107
+ {"name": "typescript", "manifest": "package.json", "primary": True},
108
+ {"name": "python", "manifest": "pyproject.toml", "primary": False},
109
+ ],
110
+ "frameworks": [{"name": "react", "language": "typescript"}],
111
+ "build_tools": [{"name": "npm", "detected_by": "lock_file"}],
112
+ },
113
+ "git": {
114
+ "_source": "scanner:git",
115
+ "platform": "github",
116
+ "remotes": [{"name": "origin", "url": "git@github.com:o/r.git"}],
117
+ "default_branch": "main",
118
+ },
119
+ "environment": {
120
+ "_source": "scanner:environment",
121
+ "os": {"platform": "linux", "architecture": "x64", "wsl": True},
122
+ "runtimes": [{"name": "python3", "version": "3.12.0"}],
123
+ "env_files": [{"name": ".env", "path": ".env"}],
124
+ },
125
+ }
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Rule 1: Scanner-owned section fully replaced
130
+ # ---------------------------------------------------------------------------
131
+
132
+
133
+ class TestScannerOwnedSectionReplacement:
134
+ """Test that scanner-owned sections are fully replaced with new data."""
135
+
136
+ def test_project_identity_replaced(self) -> None:
137
+ existing = _make_existing_context()
138
+ scan = _make_scan_results()
139
+ result = merge_context(existing, scan, _section_owners())
140
+ assert result["project_identity"]["name"] == "new-name"
141
+ assert result["project_identity"]["type"] == "monorepo"
142
+
143
+ def test_stack_section_replaced(self) -> None:
144
+ existing = _make_existing_context()
145
+ scan = _make_scan_results()
146
+ result = merge_context(existing, scan, _section_owners())
147
+ lang_names = [l["name"] for l in result["stack"]["languages"]]
148
+ assert "typescript" in lang_names
149
+ assert len(result["stack"]["frameworks"]) == 1
150
+
151
+ def test_git_section_replaced(self) -> None:
152
+ existing = _make_existing_context()
153
+ scan = _make_scan_results()
154
+ result = merge_context(existing, scan, _section_owners())
155
+ assert len(result["git"]["remotes"]) == 1
156
+
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Rule 2: Agent-enriched sections preserved
160
+ # ---------------------------------------------------------------------------
161
+
162
+
163
+ class TestAgentEnrichedPreservation:
164
+ """Test that agent-enriched sections are preserved byte-identical."""
165
+
166
+ def test_operational_guidelines_preserved(self) -> None:
167
+ existing = _make_existing_context()
168
+ scan = _make_scan_results()
169
+ result = merge_context(existing, scan, _section_owners())
170
+ assert result["operational_guidelines"]["deployment_strategy"] == "blue-green"
171
+ assert result["operational_guidelines"]["rollback_procedure"] == "manual"
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Rule 4: Mixed section (environment) sub-key merge
176
+ # ---------------------------------------------------------------------------
177
+
178
+
179
+ class TestMixedSectionMerge:
180
+ """Test that mixed sections merge scanner fields and keep agent fields."""
181
+
182
+ def test_environment_scanner_fields_refreshed(self) -> None:
183
+ existing = _make_existing_context()
184
+ scan = _make_scan_results()
185
+ result = merge_context(existing, scan, _section_owners())
186
+ # Scanner-owned sub-keys should be updated
187
+ assert result["environment"]["os"].get("wsl") is True
188
+ runtimes = result["environment"]["runtimes"]
189
+ py = [r for r in runtimes if r["name"] == "python3"][0]
190
+ assert py["version"] == "3.12.0"
191
+
192
+ def test_environment_agent_fields_kept(self) -> None:
193
+ existing = _make_existing_context()
194
+ scan = _make_scan_results()
195
+ result = merge_context(existing, scan, _section_owners())
196
+ # tools and tool_preferences came from tool scanner (not in this scan)
197
+ # They should be preserved from existing
198
+ assert "tools" in result["environment"] or "tool_preferences" in result["environment"]
199
+
200
+
201
+ # ---------------------------------------------------------------------------
202
+ # Rule 5: Unknown/user-custom sections preserved
203
+ # ---------------------------------------------------------------------------
204
+
205
+
206
+ class TestUserCustomPreservation:
207
+ """Test that user-custom sections survive combining."""
208
+
209
+ def test_custom_section_preserved(self) -> None:
210
+ existing = _make_existing_context()
211
+ scan = _make_scan_results()
212
+ result = merge_context(existing, scan, _section_owners())
213
+ assert "my_custom_notes" in result
214
+ assert result["my_custom_notes"]["author"] == "user"
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # v1-to-v2 upgrade
219
+ # ---------------------------------------------------------------------------
220
+
221
+
222
+ class TestV1ToV2Upgrade:
223
+ """Test upgrading from v1 project-context (no scan_config)."""
224
+
225
+ def test_v1_context_upgraded(self, sample_project_context_v1: Dict[str, Any]) -> None:
226
+ scan = _make_scan_results()
227
+ result = merge_context(sample_project_context_v1, scan, _section_owners())
228
+ # Should have scan_config after upgrade
229
+ assert "metadata" in result
230
+ # Agent-enriched data from v1 should be preserved
231
+ if "operational_guidelines" in sample_project_context_v1:
232
+ assert "operational_guidelines" in result
233
+
234
+ def test_v1_agent_data_not_lost(self, sample_project_context_v1: Dict[str, Any]) -> None:
235
+ scan = _make_scan_results()
236
+ result = merge_context(sample_project_context_v1, scan, _section_owners())
237
+ # User-custom sections from v1 are preserved as-is (Rule 4).
238
+ # project_details is no longer produced by the scanner (no backward compat),
239
+ # but if it existed in the v1 context it is preserved as a user-custom section.
240
+ if "project_details" in sample_project_context_v1:
241
+ assert "project_details" in result
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # Idempotency
246
+ # ---------------------------------------------------------------------------
247
+
248
+
249
+ class TestIdempotency:
250
+ """Test that running combine twice produces same result (except timestamps)."""
251
+
252
+ def test_idempotent_combine(self) -> None:
253
+ existing = _make_existing_context()
254
+ scan = _make_scan_results()
255
+
256
+ result1 = merge_context(existing, scan, _section_owners())
257
+ result2 = merge_context(result1, scan, _section_owners())
258
+
259
+ # Strip timestamps for comparison
260
+ def strip_timestamps(d: Dict) -> Dict:
261
+ d = copy.deepcopy(d)
262
+ if "metadata" in d:
263
+ meta = d["metadata"]
264
+ meta.pop("last_updated", None)
265
+ if "scan_config" in meta:
266
+ meta["scan_config"].pop("last_scan", None)
267
+ return d
268
+
269
+ assert strip_timestamps(result1) == strip_timestamps(result2)
@@ -0,0 +1,304 @@
1
+ """
2
+ Unit tests for the Orchestration Scanner (T023).
3
+
4
+ Tests K8s manifest detection, Helm chart detection, Kustomize detection,
5
+ Flux/ArgoCD detection, service mesh detection, and empty project behavior.
6
+ """
7
+
8
+ import os
9
+ import textwrap
10
+ from pathlib import Path
11
+ from typing import Any, Dict
12
+ from unittest.mock import patch
13
+
14
+ import pytest
15
+
16
+ from tools.scan.scanners.orchestration import OrchestrationScanner
17
+
18
+
19
+ @pytest.fixture
20
+ def scanner() -> OrchestrationScanner:
21
+ """Create an OrchestrationScanner instance."""
22
+ return OrchestrationScanner()
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Scanner basics
27
+ # ---------------------------------------------------------------------------
28
+
29
+
30
+ class TestOrchScannerBasics:
31
+ """Test scanner metadata and basic contract."""
32
+
33
+ def test_scanner_name(self, scanner: OrchestrationScanner) -> None:
34
+ assert scanner.SCANNER_NAME == "orchestration"
35
+
36
+ def test_scanner_version(self, scanner: OrchestrationScanner) -> None:
37
+ assert scanner.SCANNER_VERSION == "1.0.0"
38
+
39
+ def test_owned_sections(self, scanner: OrchestrationScanner) -> None:
40
+ assert scanner.OWNED_SECTIONS == ["orchestration"]
41
+
42
+ def test_source_tag(self, scanner: OrchestrationScanner) -> None:
43
+ assert scanner.source_tag == "scanner:orchestration"
44
+
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Empty project
48
+ # ---------------------------------------------------------------------------
49
+
50
+
51
+ class TestEmptyProject:
52
+ """Test empty project returns empty dict.
53
+
54
+ Must mock kubeconfig detection to prevent the host system's
55
+ ~/.kube/config from being picked up.
56
+ """
57
+
58
+ def test_empty_project_returns_empty_sections(
59
+ self, scanner: OrchestrationScanner, empty_project: Path
60
+ ) -> None:
61
+ with patch.dict(os.environ, {"KUBECONFIG": ""}, clear=False):
62
+ with patch(
63
+ "tools.scan.scanners.orchestration.Path.home",
64
+ return_value=empty_project / "_fake_home",
65
+ ):
66
+ result = scanner.scan(empty_project)
67
+ assert result.sections == {}
68
+
69
+ def test_no_orchestration_in_empty(
70
+ self, scanner: OrchestrationScanner, empty_project: Path
71
+ ) -> None:
72
+ with patch.dict(os.environ, {"KUBECONFIG": ""}, clear=False):
73
+ with patch(
74
+ "tools.scan.scanners.orchestration.Path.home",
75
+ return_value=empty_project / "_fake_home",
76
+ ):
77
+ result = scanner.scan(empty_project)
78
+ assert "orchestration" not in result.sections
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # Kubernetes detection
83
+ # ---------------------------------------------------------------------------
84
+
85
+
86
+ class TestKubernetesDetection:
87
+ """Test Kubernetes manifest detection."""
88
+
89
+ def test_detect_deployment_kind(
90
+ self, scanner: OrchestrationScanner, k8s_project: Path
91
+ ) -> None:
92
+ result = scanner.scan(k8s_project)
93
+ orch = result.sections["orchestration"]
94
+ assert orch["kubernetes"]["detected"] is True
95
+ assert "Deployment" in orch["kubernetes"]["manifest_patterns"]
96
+
97
+ def test_detect_service_kind(
98
+ self, scanner: OrchestrationScanner, k8s_project: Path
99
+ ) -> None:
100
+ result = scanner.scan(k8s_project)
101
+ orch = result.sections["orchestration"]
102
+ assert "Service" in orch["kubernetes"]["manifest_patterns"]
103
+
104
+ def test_detect_statefulset(
105
+ self, scanner: OrchestrationScanner, tmp_path: Path
106
+ ) -> None:
107
+ manifests = tmp_path / "k8s"
108
+ manifests.mkdir()
109
+ (manifests / "statefulset.yaml").write_text(
110
+ "apiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: db\n"
111
+ )
112
+ result = scanner.scan(tmp_path)
113
+ orch = result.sections["orchestration"]
114
+ assert orch["kubernetes"]["detected"] is True
115
+ assert "StatefulSet" in orch["kubernetes"]["manifest_patterns"]
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Helm detection
120
+ # ---------------------------------------------------------------------------
121
+
122
+
123
+ class TestHelmDetection:
124
+ """Test Helm chart detection."""
125
+
126
+ def test_detect_helm_chart(
127
+ self, scanner: OrchestrationScanner, helm_project: Path
128
+ ) -> None:
129
+ result = scanner.scan(helm_project)
130
+ orch = result.sections["orchestration"]
131
+ assert orch["helm"]["detected"] is True
132
+ assert len(orch["helm"]["charts"]) >= 1
133
+
134
+ def test_helm_chart_path_recorded(
135
+ self, scanner: OrchestrationScanner, helm_project: Path
136
+ ) -> None:
137
+ result = scanner.scan(helm_project)
138
+ orch = result.sections["orchestration"]
139
+ charts = orch["helm"]["charts"]
140
+ assert any("Chart.yaml" in c for c in charts)
141
+
142
+
143
+ # ---------------------------------------------------------------------------
144
+ # Kustomize detection
145
+ # ---------------------------------------------------------------------------
146
+
147
+
148
+ class TestKustomizeDetection:
149
+ """Test Kustomize detection."""
150
+
151
+ def test_detect_kustomize(
152
+ self, scanner: OrchestrationScanner, tmp_path: Path
153
+ ) -> None:
154
+ k8s_dir = tmp_path / "base"
155
+ k8s_dir.mkdir()
156
+ (k8s_dir / "kustomization.yaml").write_text(
157
+ "apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\n"
158
+ )
159
+ result = scanner.scan(tmp_path)
160
+ orch = result.sections["orchestration"]
161
+ assert orch["kustomize"]["detected"] is True
162
+
163
+ def test_kustomize_files_recorded(
164
+ self, scanner: OrchestrationScanner, tmp_path: Path
165
+ ) -> None:
166
+ k8s_dir = tmp_path / "overlays" / "dev"
167
+ k8s_dir.mkdir(parents=True)
168
+ (k8s_dir / "kustomization.yaml").write_text("resources:\n - ../../base\n")
169
+ result = scanner.scan(tmp_path)
170
+ orch = result.sections["orchestration"]
171
+ assert len(orch["kustomize"]["files"]) >= 1
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # Flux detection
176
+ # ---------------------------------------------------------------------------
177
+
178
+
179
+ class TestFluxDetection:
180
+ """Test Flux GitOps detection."""
181
+
182
+ def test_detect_flux_from_api_group(
183
+ self, scanner: OrchestrationScanner, flux_project: Path
184
+ ) -> None:
185
+ result = scanner.scan(flux_project)
186
+ orch = result.sections["orchestration"]
187
+ assert orch["gitops"]["tool"] == "flux"
188
+
189
+ def test_flux_api_groups_recorded(
190
+ self, scanner: OrchestrationScanner, flux_project: Path
191
+ ) -> None:
192
+ result = scanner.scan(flux_project)
193
+ orch = result.sections["orchestration"]
194
+ assert len(orch["gitops"]["api_groups"]) >= 1
195
+ assert any("toolkit.fluxcd.io" in g for g in orch["gitops"]["api_groups"])
196
+
197
+ def test_detect_flux_from_directory_conventions(
198
+ self, scanner: OrchestrationScanner, tmp_path: Path
199
+ ) -> None:
200
+ # Create 2 of 3 Flux convention dirs (clusters + infrastructure)
201
+ (tmp_path / "clusters").mkdir()
202
+ (tmp_path / "infrastructure").mkdir()
203
+ result = scanner.scan(tmp_path)
204
+ orch = result.sections["orchestration"]
205
+ assert orch["gitops"]["tool"] == "flux"
206
+
207
+
208
+ # ---------------------------------------------------------------------------
209
+ # ArgoCD detection
210
+ # ---------------------------------------------------------------------------
211
+
212
+
213
+ class TestArgoCDDetection:
214
+ """Test ArgoCD detection."""
215
+
216
+ def test_detect_argocd_from_api_group(
217
+ self, scanner: OrchestrationScanner, argocd_project: Path
218
+ ) -> None:
219
+ result = scanner.scan(argocd_project)
220
+ orch = result.sections["orchestration"]
221
+ assert orch["gitops"]["tool"] == "argocd"
222
+
223
+ def test_argocd_api_groups_recorded(
224
+ self, scanner: OrchestrationScanner, argocd_project: Path
225
+ ) -> None:
226
+ result = scanner.scan(argocd_project)
227
+ orch = result.sections["orchestration"]
228
+ assert any("argoproj.io" in g for g in orch["gitops"]["api_groups"])
229
+
230
+
231
+ # ---------------------------------------------------------------------------
232
+ # Service mesh detection
233
+ # ---------------------------------------------------------------------------
234
+
235
+
236
+ class TestServiceMeshDetection:
237
+ """Test service mesh detection."""
238
+
239
+ def test_detect_istio(
240
+ self, scanner: OrchestrationScanner, istio_project: Path
241
+ ) -> None:
242
+ result = scanner.scan(istio_project)
243
+ orch = result.sections["orchestration"]
244
+ assert orch["service_mesh"]["tool"] == "istio"
245
+
246
+ def test_istio_indicators_recorded(
247
+ self, scanner: OrchestrationScanner, istio_project: Path
248
+ ) -> None:
249
+ result = scanner.scan(istio_project)
250
+ orch = result.sections["orchestration"]
251
+ assert len(orch["service_mesh"]["indicators"]) >= 1
252
+
253
+ def test_detect_linkerd(
254
+ self, scanner: OrchestrationScanner, linkerd_project: Path
255
+ ) -> None:
256
+ result = scanner.scan(linkerd_project)
257
+ orch = result.sections["orchestration"]
258
+ assert orch["service_mesh"]["tool"] == "linkerd"
259
+
260
+ def test_detect_consul(
261
+ self, scanner: OrchestrationScanner, tmp_path: Path
262
+ ) -> None:
263
+ manifests = tmp_path / "k8s"
264
+ manifests.mkdir()
265
+ (manifests / "deployment.yaml").write_text(
266
+ textwrap.dedent("""\
267
+ apiVersion: apps/v1
268
+ kind: Deployment
269
+ metadata:
270
+ name: test
271
+ annotations:
272
+ consul.hashicorp.com/connect-inject: "true"
273
+ """)
274
+ )
275
+ result = scanner.scan(tmp_path)
276
+ orch = result.sections["orchestration"]
277
+ assert orch["service_mesh"]["tool"] == "consul"
278
+
279
+
280
+ # ---------------------------------------------------------------------------
281
+ # ScanResult contract
282
+ # ---------------------------------------------------------------------------
283
+
284
+
285
+ class TestOrchResultContract:
286
+ """Test scan result follows expected contract."""
287
+
288
+ def test_source_tag_present(
289
+ self, scanner: OrchestrationScanner, k8s_project: Path
290
+ ) -> None:
291
+ result = scanner.scan(k8s_project)
292
+ assert result.sections["orchestration"]["_source"] == "scanner:orchestration"
293
+
294
+ def test_result_has_duration(
295
+ self, scanner: OrchestrationScanner, k8s_project: Path
296
+ ) -> None:
297
+ result = scanner.scan(k8s_project)
298
+ assert result.duration_ms >= 0
299
+
300
+ def test_result_scanner_name(
301
+ self, scanner: OrchestrationScanner, k8s_project: Path
302
+ ) -> None:
303
+ result = scanner.scan(k8s_project)
304
+ assert result.scanner == "orchestration"