@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +12 -3
- package/ARCHITECTURE.md +9 -8
- package/CHANGELOG.md +34 -0
- package/README.md +43 -11
- package/agents/terraform-architect.md +1 -1
- package/bin/README.md +2 -2
- package/bin/gaia-doctor.js +18 -5
- package/bin/gaia-history.js +0 -1
- package/bin/gaia-metrics.js +2 -2
- package/bin/gaia-scan.py +23 -1
- package/bin/gaia-update.js +346 -54
- package/bin/pre-publish-validate.js +33 -10
- package/commands/gaia.md +37 -0
- package/config/README.md +3 -9
- package/config/context-contracts.json +47 -15
- package/config/surface-routing.json +9 -1
- package/dist/gaia-ops/.claude-plugin/plugin.json +22 -0
- package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
- package/dist/gaia-ops/agents/devops-developer.md +57 -0
- package/dist/gaia-ops/agents/gaia-system.md +58 -0
- package/dist/gaia-ops/agents/gitops-operator.md +60 -0
- package/dist/gaia-ops/agents/speckit-planner.md +71 -0
- package/dist/gaia-ops/agents/terraform-architect.md +60 -0
- package/dist/gaia-ops/commands/gaia.md +37 -0
- package/dist/gaia-ops/config/README.md +58 -0
- package/dist/gaia-ops/config/cloud/aws.json +140 -0
- package/dist/gaia-ops/config/cloud/gcp.json +145 -0
- package/dist/gaia-ops/config/context-contracts.json +131 -0
- package/dist/gaia-ops/config/git_standards.json +72 -0
- package/dist/gaia-ops/config/surface-routing.json +197 -0
- package/dist/gaia-ops/config/universal-rules.json +10 -0
- package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-ops/hooks/adapters/base.py +219 -0
- package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
- package/dist/gaia-ops/hooks/adapters/claude_code.py +1477 -0
- package/dist/gaia-ops/hooks/adapters/types.py +194 -0
- package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
- package/dist/gaia-ops/hooks/hooks.json +126 -0
- package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
- package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +124 -0
- package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +576 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +215 -0
- package/dist/gaia-ops/hooks/modules/context/context_cache.py +129 -0
- package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +427 -0
- package/dist/gaia-ops/hooks/modules/context/context_writer.py +518 -0
- package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +558 -0
- package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
- package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-ops/hooks/modules/identity/__init__.py +0 -0
- package/dist/gaia-ops/hooks/modules/identity/identity_provider.py +21 -0
- package/dist/gaia-ops/hooks/modules/identity/ops_identity.py +34 -0
- package/dist/gaia-ops/hooks/modules/identity/security_identity.py +10 -0
- package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +227 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-ops/hooks/modules/security/__init__.py +89 -0
- package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +912 -0
- package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +153 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +584 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +86 -0
- package/dist/gaia-ops/hooks/modules/security/command_semantics.py +130 -0
- package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +850 -0
- package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +158 -0
- package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-ops/hooks/modules/tools/__init__.py +25 -0
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +708 -0
- package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +181 -0
- package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-ops/hooks/modules/tools/task_validator.py +283 -0
- package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-ops/hooks/post_compact.py +43 -0
- package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
- package/dist/gaia-ops/hooks/pre_tool_use.py +383 -0
- package/dist/gaia-ops/hooks/session_start.py +69 -0
- package/dist/gaia-ops/hooks/stop_hook.py +69 -0
- package/dist/gaia-ops/hooks/subagent_start.py +71 -0
- package/dist/gaia-ops/hooks/subagent_stop.py +288 -0
- package/dist/gaia-ops/hooks/task_completed.py +70 -0
- package/dist/gaia-ops/hooks/user_prompt_submit.py +177 -0
- package/dist/gaia-ops/settings.json +72 -0
- package/dist/gaia-ops/skills/README.md +109 -0
- package/dist/gaia-ops/skills/agent-protocol/SKILL.md +105 -0
- package/dist/gaia-ops/skills/agent-protocol/examples.md +170 -0
- package/dist/gaia-ops/skills/agent-response/SKILL.md +53 -0
- package/dist/gaia-ops/skills/approval/SKILL.md +85 -0
- package/dist/gaia-ops/skills/approval/examples.md +140 -0
- package/dist/gaia-ops/skills/approval/reference.md +57 -0
- package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
- package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
- package/dist/gaia-ops/skills/context-updater/SKILL.md +76 -0
- package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
- package/dist/gaia-ops/skills/developer-patterns/SKILL.md +93 -0
- package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
- package/dist/gaia-ops/skills/execution/SKILL.md +66 -0
- package/dist/gaia-ops/skills/fast-queries/SKILL.md +47 -0
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +92 -0
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +22 -0
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +48 -0
- package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +73 -0
- package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
- package/dist/gaia-ops/skills/investigation/SKILL.md +77 -0
- package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +64 -0
- package/dist/gaia-ops/skills/reference.md +134 -0
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +61 -0
- package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
- package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
- package/dist/gaia-ops/skills/skill-creation/SKILL.md +119 -0
- package/dist/gaia-ops/skills/specification/SKILL.md +186 -0
- package/dist/gaia-ops/skills/speckit-workflow/SKILL.md +165 -0
- package/dist/gaia-ops/skills/speckit-workflow/reference.md +117 -0
- package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +63 -0
- package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
- package/dist/gaia-ops/speckit/README.md +516 -0
- package/dist/gaia-ops/speckit/scripts/.gitkeep +0 -0
- package/dist/gaia-ops/speckit/templates/adr-template.md +118 -0
- package/dist/gaia-ops/speckit/templates/agent-file-template.md +23 -0
- package/dist/gaia-ops/speckit/templates/plan-template.md +227 -0
- package/dist/gaia-ops/speckit/templates/spec-template.md +140 -0
- package/dist/gaia-ops/speckit/templates/tasks-template.md +257 -0
- package/dist/gaia-ops/tools/context/README.md +132 -0
- package/dist/gaia-ops/tools/context/__init__.py +42 -0
- package/dist/gaia-ops/tools/context/_paths.py +20 -0
- package/dist/gaia-ops/tools/context/context_provider.py +476 -0
- package/dist/gaia-ops/tools/context/context_section_reader.py +330 -0
- package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
- package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
- package/dist/gaia-ops/tools/context/surface_router.py +278 -0
- package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
- package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
- package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
- package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
- package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
- package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
- package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
- package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
- package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
- package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
- package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
- package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
- package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +262 -0
- package/dist/gaia-ops/tools/memory/README.md +0 -0
- package/dist/gaia-ops/tools/memory/__init__.py +20 -0
- package/dist/gaia-ops/tools/memory/episodic.py +1196 -0
- package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
- package/dist/gaia-ops/tools/review/__init__.py +1 -0
- package/dist/gaia-ops/tools/review/review_engine.py +157 -0
- package/dist/gaia-ops/tools/scan/__init__.py +35 -0
- package/dist/gaia-ops/tools/scan/config.py +247 -0
- package/dist/gaia-ops/tools/scan/merge.py +212 -0
- package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
- package/dist/gaia-ops/tools/scan/registry.py +127 -0
- package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
- package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
- package/dist/gaia-ops/tools/scan/scanners/environment.py +324 -0
- package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
- package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
- package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
- package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
- package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
- package/dist/gaia-ops/tools/scan/setup.py +753 -0
- package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
- package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
- package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
- package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
- package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
- package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
- package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
- package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
- package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
- package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
- package/dist/gaia-ops/tools/scan/ui.py +624 -0
- package/dist/gaia-ops/tools/scan/verify.py +266 -0
- package/dist/gaia-ops/tools/scan/walk.py +118 -0
- package/dist/gaia-ops/tools/scan/workspace.py +85 -0
- package/dist/gaia-ops/tools/validation/README.md +244 -0
- package/dist/gaia-ops/tools/validation/__init__.py +17 -0
- package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
- package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
- package/dist/gaia-security/.claude-plugin/plugin.json +22 -0
- package/dist/gaia-security/config/universal-rules.json +10 -0
- package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-security/hooks/adapters/base.py +219 -0
- package/dist/gaia-security/hooks/adapters/channel.py +17 -0
- package/dist/gaia-security/hooks/adapters/claude_code.py +1477 -0
- package/dist/gaia-security/hooks/adapters/types.py +194 -0
- package/dist/gaia-security/hooks/adapters/utils.py +25 -0
- package/dist/gaia-security/hooks/hooks.json +57 -0
- package/dist/gaia-security/hooks/modules/__init__.py +15 -0
- package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +124 -0
- package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +576 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +215 -0
- package/dist/gaia-security/hooks/modules/context/context_cache.py +129 -0
- package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-security/hooks/modules/context/context_injector.py +427 -0
- package/dist/gaia-security/hooks/modules/context/context_writer.py +518 -0
- package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-security/hooks/modules/core/plugin_setup.py +558 -0
- package/dist/gaia-security/hooks/modules/core/state.py +179 -0
- package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-security/hooks/modules/identity/__init__.py +0 -0
- package/dist/gaia-security/hooks/modules/identity/identity_provider.py +21 -0
- package/dist/gaia-security/hooks/modules/identity/ops_identity.py +34 -0
- package/dist/gaia-security/hooks/modules/identity/security_identity.py +10 -0
- package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/memory/episode_writer.py +227 -0
- package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-security/hooks/modules/security/__init__.py +89 -0
- package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +912 -0
- package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-security/hooks/modules/security/approval_scopes.py +153 -0
- package/dist/gaia-security/hooks/modules/security/blocked_commands.py +584 -0
- package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +86 -0
- package/dist/gaia-security/hooks/modules/security/command_semantics.py +130 -0
- package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +850 -0
- package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-security/hooks/modules/session/session_event_injector.py +158 -0
- package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-security/hooks/modules/tools/__init__.py +25 -0
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +708 -0
- package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +181 -0
- package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-security/hooks/modules/tools/task_validator.py +283 -0
- package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-security/hooks/post_tool_use.py +54 -0
- package/dist/gaia-security/hooks/pre_tool_use.py +383 -0
- package/dist/gaia-security/hooks/session_start.py +69 -0
- package/dist/gaia-security/hooks/stop_hook.py +69 -0
- package/dist/gaia-security/hooks/user_prompt_submit.py +177 -0
- package/dist/gaia-security/settings.json +58 -0
- package/git-hooks/commit-msg +41 -0
- package/hooks/README.md +8 -6
- package/hooks/adapters/channel.py +0 -25
- package/hooks/adapters/claude_code.py +364 -125
- package/hooks/elicitation_result.py +132 -0
- package/hooks/hooks.json +10 -1
- package/hooks/modules/README.md +3 -2
- package/hooks/modules/agents/contract_validator.py +3 -51
- package/hooks/modules/agents/response_contract.py +4 -8
- package/hooks/modules/agents/transcript_reader.py +4 -5
- package/hooks/modules/audit/__init__.py +4 -6
- package/hooks/modules/audit/event_detector.py +0 -2
- package/hooks/modules/audit/metrics.py +108 -187
- package/hooks/modules/audit/workflow_auditor.py +0 -4
- package/hooks/modules/audit/workflow_recorder.py +0 -5
- package/hooks/modules/context/compact_context_builder.py +1 -0
- package/hooks/modules/context/context_cache.py +129 -0
- package/hooks/modules/context/context_injector.py +18 -40
- package/hooks/modules/context/context_writer.py +1 -25
- package/hooks/modules/context/contracts_loader.py +7 -10
- package/hooks/modules/core/hook_entry.py +1 -0
- package/hooks/modules/core/paths.py +12 -13
- package/hooks/modules/core/plugin_mode.py +74 -4
- package/hooks/modules/core/plugin_setup.py +395 -23
- package/hooks/modules/events/__init__.py +1 -0
- package/hooks/modules/events/event_writer.py +210 -0
- package/hooks/modules/identity/ops_identity.py +18 -27
- package/hooks/modules/memory/episode_writer.py +1 -6
- package/hooks/modules/orchestrator/__init__.py +1 -0
- package/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/hooks/modules/security/__init__.py +2 -4
- package/hooks/modules/security/approval_constants.py +5 -1
- package/hooks/modules/security/approval_grants.py +189 -6
- package/hooks/modules/security/approval_messages.py +9 -21
- package/hooks/modules/security/blocked_commands.py +98 -34
- package/hooks/modules/security/command_semantics.py +0 -4
- package/hooks/modules/security/gitops_validator.py +1 -11
- package/hooks/modules/security/mutative_verbs.py +179 -38
- package/hooks/modules/security/tiers.py +1 -19
- package/hooks/modules/session/session_event_injector.py +1 -25
- package/hooks/modules/tools/bash_validator.py +310 -94
- package/hooks/modules/tools/shell_parser.py +0 -1
- package/hooks/modules/tools/task_validator.py +9 -29
- package/hooks/post_tool_use.py +0 -72
- package/hooks/pre_tool_use.py +42 -102
- package/hooks/session_start.py +4 -2
- package/hooks/subagent_start.py +6 -2
- package/hooks/subagent_stop.py +1 -13
- package/hooks/user_prompt_submit.py +119 -37
- package/index.js +1 -1
- package/package.json +5 -3
- package/skills/README.md +3 -5
- package/skills/agent-protocol/SKILL.md +17 -16
- package/skills/agent-protocol/examples.md +6 -6
- package/skills/agent-response/SKILL.md +11 -14
- package/skills/approval/SKILL.md +28 -13
- package/skills/approval/reference.md +2 -2
- package/skills/execution/SKILL.md +1 -1
- package/skills/gaia-patterns/SKILL.md +2 -3
- package/skills/orchestrator-approval/SKILL.md +22 -50
- package/skills/security-tiers/SKILL.md +1 -1
- package/templates/README.md +9 -9
- package/templates/managed-settings.template.json +43 -0
- package/tools/gaia_simulator/runner.py +34 -1
- package/tools/scan/orchestrator.py +13 -0
- package/tools/scan/scanners/base.py +8 -0
- package/tools/scan/scanners/git.py +78 -0
- package/tools/scan/scanners/infrastructure.py +65 -0
- package/tools/scan/scanners/stack.py +110 -0
- package/tools/scan/setup.py +120 -13
- package/tools/scan/workspace.py +85 -0
- package/config/context-contracts.aws.json +0 -42
- package/config/context-contracts.gcp.json +0 -39
- package/skills/project-dispatch/SKILL.md +0 -34
- 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"
|