@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,600 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Orchestration Scanner
|
|
3
|
+
|
|
4
|
+
Detects Kubernetes, GitOps, Helm, Kustomize, and service mesh indicators
|
|
5
|
+
from project filesystem. Only produces output when orchestration tooling
|
|
6
|
+
is detected -- returns empty dict for projects with no orchestration files.
|
|
7
|
+
|
|
8
|
+
Contract: specs/002-gaia-scan/data-model.md section 2.8
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from tools.scan.scanners.base import BaseScanner, ScanResult
|
|
18
|
+
from tools.scan.walk import walk_project, walk_project_named
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Kubernetes manifest kinds that indicate orchestration
|
|
23
|
+
_K8S_KINDS = frozenset({
|
|
24
|
+
"Deployment",
|
|
25
|
+
"Service",
|
|
26
|
+
"StatefulSet",
|
|
27
|
+
"DaemonSet",
|
|
28
|
+
"Job",
|
|
29
|
+
"CronJob",
|
|
30
|
+
"Ingress",
|
|
31
|
+
"ConfigMap",
|
|
32
|
+
"Secret",
|
|
33
|
+
"HelmRelease",
|
|
34
|
+
"Kustomization",
|
|
35
|
+
"Pod",
|
|
36
|
+
"ReplicaSet",
|
|
37
|
+
"Namespace",
|
|
38
|
+
"ServiceAccount",
|
|
39
|
+
"ClusterRole",
|
|
40
|
+
"ClusterRoleBinding",
|
|
41
|
+
"Role",
|
|
42
|
+
"RoleBinding",
|
|
43
|
+
"PersistentVolumeClaim",
|
|
44
|
+
"PersistentVolume",
|
|
45
|
+
"NetworkPolicy",
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
# GitOps API groups
|
|
49
|
+
_FLUX_API_GROUPS = frozenset({
|
|
50
|
+
"toolkit.fluxcd.io",
|
|
51
|
+
"source.toolkit.fluxcd.io",
|
|
52
|
+
"kustomize.toolkit.fluxcd.io",
|
|
53
|
+
"helm.toolkit.fluxcd.io",
|
|
54
|
+
"notification.toolkit.fluxcd.io",
|
|
55
|
+
"image.toolkit.fluxcd.io",
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
_ARGOCD_API_GROUPS = frozenset({
|
|
59
|
+
"argoproj.io",
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
# Flux directory conventions
|
|
63
|
+
_FLUX_DIR_CONVENTIONS = frozenset({
|
|
64
|
+
"clusters",
|
|
65
|
+
"infrastructure",
|
|
66
|
+
"apps",
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
# Service mesh annotation prefixes
|
|
70
|
+
_ISTIO_INDICATORS = frozenset({
|
|
71
|
+
"sidecar.istio.io",
|
|
72
|
+
"istio.io",
|
|
73
|
+
"networking.istio.io",
|
|
74
|
+
"security.istio.io",
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
_LINKERD_INDICATORS = frozenset({
|
|
78
|
+
"linkerd.io",
|
|
79
|
+
"viz.linkerd.io",
|
|
80
|
+
"config.linkerd.io",
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
_CONSUL_INDICATORS = frozenset({
|
|
84
|
+
"consul.hashicorp.com",
|
|
85
|
+
"connect-inject",
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
# Maximum number of YAML files to scan to stay within performance budget
|
|
89
|
+
_MAX_YAML_FILES = 500
|
|
90
|
+
|
|
91
|
+
# Maximum file size (bytes) to read for YAML scanning
|
|
92
|
+
_MAX_YAML_SIZE = 256 * 1024 # 256 KB
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class OrchestrationScanner(BaseScanner):
|
|
96
|
+
"""Scanner for Kubernetes, GitOps, Helm, Kustomize, and service mesh.
|
|
97
|
+
|
|
98
|
+
Only creates the 'orchestration' section when indicators are found.
|
|
99
|
+
Returns empty dict for projects with no orchestration tooling.
|
|
100
|
+
|
|
101
|
+
Pure Function Contract:
|
|
102
|
+
- No file writes
|
|
103
|
+
- No state modification
|
|
104
|
+
- No network calls
|
|
105
|
+
- No command execution
|
|
106
|
+
- Only filesystem reads
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def SCANNER_NAME(self) -> str:
|
|
111
|
+
return "orchestration"
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def SCANNER_VERSION(self) -> str:
|
|
115
|
+
return "1.0.0"
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def OWNED_SECTIONS(self) -> List[str]:
|
|
119
|
+
return ["orchestration"]
|
|
120
|
+
|
|
121
|
+
def scan(self, root: Path) -> ScanResult:
|
|
122
|
+
"""Scan the project for orchestration indicators.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
root: Absolute path to the project root directory.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
ScanResult with 'orchestration' section if indicators found,
|
|
129
|
+
or ScanResult with empty sections if nothing detected.
|
|
130
|
+
"""
|
|
131
|
+
start_ms = time.monotonic() * 1000
|
|
132
|
+
warnings: List[str] = []
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
kubernetes = self._detect_kubernetes(root, warnings)
|
|
136
|
+
gitops = self._detect_gitops(root, warnings)
|
|
137
|
+
helm = self._detect_helm(root, warnings)
|
|
138
|
+
kustomize = self._detect_kustomize(root, warnings)
|
|
139
|
+
service_mesh = self._detect_service_mesh(root, warnings)
|
|
140
|
+
|
|
141
|
+
# Only produce section when at least one indicator is found
|
|
142
|
+
has_indicators = (
|
|
143
|
+
kubernetes["detected"]
|
|
144
|
+
or gitops["tool"] is not None
|
|
145
|
+
or helm["detected"]
|
|
146
|
+
or kustomize["detected"]
|
|
147
|
+
or service_mesh["tool"] is not None
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if not has_indicators:
|
|
151
|
+
elapsed = (time.monotonic() * 1000) - start_ms
|
|
152
|
+
return self.make_result(
|
|
153
|
+
sections={},
|
|
154
|
+
warnings=warnings,
|
|
155
|
+
duration_ms=elapsed,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
orchestration_data: Dict[str, Any] = {
|
|
159
|
+
"kubernetes": kubernetes,
|
|
160
|
+
"gitops": gitops,
|
|
161
|
+
"helm": helm,
|
|
162
|
+
"kustomize": kustomize,
|
|
163
|
+
"service_mesh": service_mesh,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
elapsed = (time.monotonic() * 1000) - start_ms
|
|
167
|
+
return self.make_result(
|
|
168
|
+
sections={"orchestration": orchestration_data},
|
|
169
|
+
warnings=warnings,
|
|
170
|
+
duration_ms=elapsed,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
elapsed = (time.monotonic() * 1000) - start_ms
|
|
175
|
+
logger.warning("Orchestration scanner failed: %s", exc)
|
|
176
|
+
return self.make_result(
|
|
177
|
+
sections={},
|
|
178
|
+
warnings=[f"Orchestration scanner error: {exc}"],
|
|
179
|
+
duration_ms=elapsed,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# ------------------------------------------------------------------ #
|
|
183
|
+
# Kubernetes Detection
|
|
184
|
+
# ------------------------------------------------------------------ #
|
|
185
|
+
|
|
186
|
+
def _detect_kubernetes(
|
|
187
|
+
self, root: Path, warnings: List[str]
|
|
188
|
+
) -> Dict[str, Any]:
|
|
189
|
+
"""Detect Kubernetes indicators from manifests, kubeconfig, etc."""
|
|
190
|
+
indicators: List[str] = []
|
|
191
|
+
manifest_patterns: List[str] = []
|
|
192
|
+
|
|
193
|
+
# Check YAML manifests for Kubernetes kinds
|
|
194
|
+
yaml_kinds = self._scan_yaml_for_kinds(root, warnings)
|
|
195
|
+
if yaml_kinds:
|
|
196
|
+
indicators.append("kubernetes manifests found")
|
|
197
|
+
manifest_patterns.extend(sorted(yaml_kinds))
|
|
198
|
+
|
|
199
|
+
# Check kubeconfig
|
|
200
|
+
kubeconfig_path = self._find_kubeconfig()
|
|
201
|
+
if kubeconfig_path:
|
|
202
|
+
indicators.append(f"kubeconfig: {kubeconfig_path}")
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
"detected": len(indicators) > 0,
|
|
206
|
+
"indicators": indicators,
|
|
207
|
+
"manifest_patterns": manifest_patterns,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
def _scan_yaml_for_kinds(
|
|
211
|
+
self, root: Path, warnings: List[str]
|
|
212
|
+
) -> List[str]:
|
|
213
|
+
"""Scan YAML files for Kubernetes resource kinds."""
|
|
214
|
+
kinds_found: set = set()
|
|
215
|
+
yaml_files = self._find_yaml_files(root)
|
|
216
|
+
|
|
217
|
+
for yaml_path in yaml_files:
|
|
218
|
+
try:
|
|
219
|
+
content = self._safe_read(yaml_path)
|
|
220
|
+
if content is None:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
for line in content.splitlines():
|
|
224
|
+
stripped = line.strip()
|
|
225
|
+
if stripped.startswith("kind:"):
|
|
226
|
+
kind_value = stripped[5:].strip().strip('"').strip("'")
|
|
227
|
+
if kind_value in _K8S_KINDS:
|
|
228
|
+
kinds_found.add(kind_value)
|
|
229
|
+
except Exception:
|
|
230
|
+
# Individual file read failures must not abort the scanner
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
return sorted(kinds_found)
|
|
234
|
+
|
|
235
|
+
def _find_kubeconfig(self) -> Optional[str]:
|
|
236
|
+
"""Check for kubeconfig presence via env var or default path."""
|
|
237
|
+
# Check KUBECONFIG env var
|
|
238
|
+
kubeconfig_env = os.environ.get("KUBECONFIG")
|
|
239
|
+
if kubeconfig_env:
|
|
240
|
+
for kc_path in kubeconfig_env.split(os.pathsep):
|
|
241
|
+
if Path(kc_path).is_file():
|
|
242
|
+
return kc_path
|
|
243
|
+
|
|
244
|
+
# Check default location
|
|
245
|
+
default_kc = Path.home() / ".kube" / "config"
|
|
246
|
+
if default_kc.is_file():
|
|
247
|
+
return str(default_kc)
|
|
248
|
+
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
# ------------------------------------------------------------------ #
|
|
252
|
+
# GitOps Detection
|
|
253
|
+
# ------------------------------------------------------------------ #
|
|
254
|
+
|
|
255
|
+
def _detect_gitops(
|
|
256
|
+
self, root: Path, warnings: List[str]
|
|
257
|
+
) -> Dict[str, Any]:
|
|
258
|
+
"""Detect Flux, ArgoCD, or other GitOps tooling."""
|
|
259
|
+
api_groups: List[str] = []
|
|
260
|
+
tool: Optional[str] = None
|
|
261
|
+
config_path: Optional[str] = None
|
|
262
|
+
|
|
263
|
+
# Scan YAML files for API groups
|
|
264
|
+
flux_groups, argocd_groups = self._scan_yaml_for_api_groups(
|
|
265
|
+
root, warnings
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
if flux_groups:
|
|
269
|
+
tool = "flux"
|
|
270
|
+
api_groups.extend(sorted(flux_groups))
|
|
271
|
+
# Look for Flux config path
|
|
272
|
+
config_path = self._find_flux_config_path(root)
|
|
273
|
+
elif argocd_groups:
|
|
274
|
+
tool = "argocd"
|
|
275
|
+
api_groups.extend(sorted(argocd_groups))
|
|
276
|
+
# Look for ArgoCD config path
|
|
277
|
+
config_path = self._find_argocd_config_path(root)
|
|
278
|
+
|
|
279
|
+
# Check Flux directory conventions if no API groups found
|
|
280
|
+
if tool is None:
|
|
281
|
+
flux_dir = self._check_flux_directory_conventions(root)
|
|
282
|
+
if flux_dir:
|
|
283
|
+
tool = "flux"
|
|
284
|
+
config_path = flux_dir
|
|
285
|
+
|
|
286
|
+
# If tool is detected but config_path is still None, derive from
|
|
287
|
+
# kustomize file paths (e.g., qxo-monorepo/gitops/base/kustomization.yaml)
|
|
288
|
+
if tool is not None and config_path is None:
|
|
289
|
+
config_path = self._derive_config_path_from_kustomize(root)
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
"tool": tool,
|
|
293
|
+
"api_groups": api_groups,
|
|
294
|
+
"config_path": config_path,
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
def _scan_yaml_for_api_groups(
|
|
298
|
+
self, root: Path, warnings: List[str]
|
|
299
|
+
) -> tuple:
|
|
300
|
+
"""Scan YAML files for Flux and ArgoCD API groups.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Tuple of (flux_groups, argocd_groups) sets.
|
|
304
|
+
"""
|
|
305
|
+
flux_groups: set = set()
|
|
306
|
+
argocd_groups: set = set()
|
|
307
|
+
|
|
308
|
+
yaml_files = self._find_yaml_files(root)
|
|
309
|
+
|
|
310
|
+
for yaml_path in yaml_files:
|
|
311
|
+
try:
|
|
312
|
+
content = self._safe_read(yaml_path)
|
|
313
|
+
if content is None:
|
|
314
|
+
continue
|
|
315
|
+
|
|
316
|
+
for line in content.splitlines():
|
|
317
|
+
stripped = line.strip()
|
|
318
|
+
|
|
319
|
+
# Check apiVersion lines for Flux
|
|
320
|
+
for fg in _FLUX_API_GROUPS:
|
|
321
|
+
if fg in stripped:
|
|
322
|
+
flux_groups.add(fg)
|
|
323
|
+
|
|
324
|
+
# Check apiVersion lines for ArgoCD
|
|
325
|
+
for ag in _ARGOCD_API_GROUPS:
|
|
326
|
+
if ag in stripped:
|
|
327
|
+
argocd_groups.add(ag)
|
|
328
|
+
except Exception:
|
|
329
|
+
continue
|
|
330
|
+
|
|
331
|
+
return flux_groups, argocd_groups
|
|
332
|
+
|
|
333
|
+
def _find_flux_config_path(self, root: Path) -> Optional[str]:
|
|
334
|
+
"""Find the Flux configuration root directory.
|
|
335
|
+
|
|
336
|
+
Checks root-level directories first, then searches subdirectories
|
|
337
|
+
for gitops-related directory names.
|
|
338
|
+
"""
|
|
339
|
+
candidates = ["clusters", "flux-system", "gitops"]
|
|
340
|
+
|
|
341
|
+
# Check root-level directories
|
|
342
|
+
for candidate in candidates:
|
|
343
|
+
candidate_path = root / candidate
|
|
344
|
+
if candidate_path.is_dir():
|
|
345
|
+
return candidate
|
|
346
|
+
|
|
347
|
+
# Check one level deeper (e.g., monorepo-root/gitops/)
|
|
348
|
+
try:
|
|
349
|
+
for entry in sorted(root.iterdir()):
|
|
350
|
+
if not entry.is_dir() or entry.name.startswith("."):
|
|
351
|
+
continue
|
|
352
|
+
if entry.name in ("node_modules", "vendor", "__pycache__"):
|
|
353
|
+
continue
|
|
354
|
+
for candidate in candidates:
|
|
355
|
+
nested = entry / candidate
|
|
356
|
+
if nested.is_dir():
|
|
357
|
+
return str(nested.relative_to(root))
|
|
358
|
+
except OSError:
|
|
359
|
+
pass
|
|
360
|
+
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
def _find_argocd_config_path(self, root: Path) -> Optional[str]:
|
|
364
|
+
"""Find the ArgoCD configuration root directory."""
|
|
365
|
+
candidates = ["argocd", "argo-cd", "applications"]
|
|
366
|
+
for candidate in candidates:
|
|
367
|
+
candidate_path = root / candidate
|
|
368
|
+
if candidate_path.is_dir():
|
|
369
|
+
return candidate
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
def _check_flux_directory_conventions(
|
|
373
|
+
self, root: Path
|
|
374
|
+
) -> Optional[str]:
|
|
375
|
+
"""Check for Flux directory conventions (clusters/, infrastructure/, apps/)."""
|
|
376
|
+
matched_dirs = []
|
|
377
|
+
for dirname in _FLUX_DIR_CONVENTIONS:
|
|
378
|
+
if (root / dirname).is_dir():
|
|
379
|
+
matched_dirs.append(dirname)
|
|
380
|
+
|
|
381
|
+
# Require at least 2 of the 3 convention directories
|
|
382
|
+
if len(matched_dirs) >= 2:
|
|
383
|
+
return "clusters" if (root / "clusters").is_dir() else matched_dirs[0]
|
|
384
|
+
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
def _derive_config_path_from_kustomize(self, root: Path) -> Optional[str]:
|
|
388
|
+
"""Derive gitops config_path from detected kustomize file locations.
|
|
389
|
+
|
|
390
|
+
Finds kustomization.yaml files and returns the highest common
|
|
391
|
+
gitops-related ancestor directory.
|
|
392
|
+
"""
|
|
393
|
+
gitops_dirs: List[str] = []
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
for kust_file in walk_project_named(
|
|
397
|
+
root, ["kustomization.yaml", "kustomization.yml", "Kustomization"]
|
|
398
|
+
):
|
|
399
|
+
rel = kust_file.relative_to(root)
|
|
400
|
+
# Walk up the path parts looking for a gitops-related directory name
|
|
401
|
+
parts = rel.parent.parts
|
|
402
|
+
for i, part in enumerate(parts):
|
|
403
|
+
if part.lower() in ("gitops", "flux", "argocd", "deploy", "k8s"):
|
|
404
|
+
gitops_path = str(Path(*parts[: i + 1]))
|
|
405
|
+
if gitops_path not in gitops_dirs:
|
|
406
|
+
gitops_dirs.append(gitops_path)
|
|
407
|
+
break
|
|
408
|
+
except Exception:
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
if gitops_dirs:
|
|
412
|
+
return gitops_dirs[0]
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
# ------------------------------------------------------------------ #
|
|
416
|
+
# Helm Detection
|
|
417
|
+
# ------------------------------------------------------------------ #
|
|
418
|
+
|
|
419
|
+
def _detect_helm(
|
|
420
|
+
self, root: Path, warnings: List[str]
|
|
421
|
+
) -> Dict[str, Any]:
|
|
422
|
+
"""Detect Helm charts from Chart.yaml files."""
|
|
423
|
+
charts: List[str] = []
|
|
424
|
+
|
|
425
|
+
try:
|
|
426
|
+
for chart_yaml in walk_project_named(root, ["Chart.yaml"]):
|
|
427
|
+
rel_path = str(chart_yaml.relative_to(root))
|
|
428
|
+
charts.append(rel_path)
|
|
429
|
+
except Exception:
|
|
430
|
+
warnings.append("Failed to scan for Helm Chart.yaml files")
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
"detected": len(charts) > 0,
|
|
434
|
+
"charts": sorted(charts),
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
# ------------------------------------------------------------------ #
|
|
438
|
+
# Kustomize Detection
|
|
439
|
+
# ------------------------------------------------------------------ #
|
|
440
|
+
|
|
441
|
+
def _detect_kustomize(
|
|
442
|
+
self, root: Path, warnings: List[str]
|
|
443
|
+
) -> Dict[str, Any]:
|
|
444
|
+
"""Detect Kustomize from kustomization.yaml files."""
|
|
445
|
+
files: List[str] = []
|
|
446
|
+
|
|
447
|
+
try:
|
|
448
|
+
for kust_file in walk_project_named(root, ["kustomization.yaml", "kustomization.yml", "Kustomization"]):
|
|
449
|
+
rel_path = str(kust_file.relative_to(root))
|
|
450
|
+
if rel_path not in files:
|
|
451
|
+
files.append(rel_path)
|
|
452
|
+
except Exception:
|
|
453
|
+
warnings.append("Failed to scan for kustomization.yaml files")
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
"detected": len(files) > 0,
|
|
457
|
+
"files": sorted(files),
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# ------------------------------------------------------------------ #
|
|
461
|
+
# Service Mesh Detection
|
|
462
|
+
# ------------------------------------------------------------------ #
|
|
463
|
+
|
|
464
|
+
def _detect_service_mesh(
|
|
465
|
+
self, root: Path, warnings: List[str]
|
|
466
|
+
) -> Dict[str, Any]:
|
|
467
|
+
"""Detect Istio, Linkerd, or Consul Connect from annotations."""
|
|
468
|
+
tool: Optional[str] = None
|
|
469
|
+
indicators: List[str] = []
|
|
470
|
+
|
|
471
|
+
istio_found = False
|
|
472
|
+
linkerd_found = False
|
|
473
|
+
consul_found = False
|
|
474
|
+
|
|
475
|
+
yaml_files = self._find_yaml_files(root)
|
|
476
|
+
|
|
477
|
+
for yaml_path in yaml_files:
|
|
478
|
+
try:
|
|
479
|
+
content = self._safe_read(yaml_path)
|
|
480
|
+
if content is None:
|
|
481
|
+
continue
|
|
482
|
+
|
|
483
|
+
for line in content.splitlines():
|
|
484
|
+
stripped = line.strip()
|
|
485
|
+
|
|
486
|
+
# Istio
|
|
487
|
+
for prefix in _ISTIO_INDICATORS:
|
|
488
|
+
if prefix in stripped:
|
|
489
|
+
istio_found = True
|
|
490
|
+
indicator = f"istio: {prefix}"
|
|
491
|
+
if indicator not in indicators:
|
|
492
|
+
indicators.append(indicator)
|
|
493
|
+
|
|
494
|
+
# Linkerd
|
|
495
|
+
for prefix in _LINKERD_INDICATORS:
|
|
496
|
+
if prefix in stripped:
|
|
497
|
+
linkerd_found = True
|
|
498
|
+
indicator = f"linkerd: {prefix}"
|
|
499
|
+
if indicator not in indicators:
|
|
500
|
+
indicators.append(indicator)
|
|
501
|
+
|
|
502
|
+
# Consul Connect
|
|
503
|
+
for prefix in _CONSUL_INDICATORS:
|
|
504
|
+
if prefix in stripped:
|
|
505
|
+
consul_found = True
|
|
506
|
+
indicator = f"consul: {prefix}"
|
|
507
|
+
if indicator not in indicators:
|
|
508
|
+
indicators.append(indicator)
|
|
509
|
+
except Exception:
|
|
510
|
+
continue
|
|
511
|
+
|
|
512
|
+
# Determine primary tool (first detected wins)
|
|
513
|
+
if istio_found:
|
|
514
|
+
tool = "istio"
|
|
515
|
+
elif linkerd_found:
|
|
516
|
+
tool = "linkerd"
|
|
517
|
+
elif consul_found:
|
|
518
|
+
tool = "consul"
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
"tool": tool,
|
|
522
|
+
"indicators": sorted(indicators),
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# ------------------------------------------------------------------ #
|
|
526
|
+
# Utility Methods
|
|
527
|
+
# ------------------------------------------------------------------ #
|
|
528
|
+
|
|
529
|
+
def _find_yaml_files(self, root: Path) -> List[Path]:
|
|
530
|
+
"""Find YAML files in the project, respecting scan limits.
|
|
531
|
+
|
|
532
|
+
Uses walk_project for filtered os.walk (skips node_modules, .git, etc.)
|
|
533
|
+
instead of rglob which traverses all directories.
|
|
534
|
+
|
|
535
|
+
Caches results on the instance for reuse across detection methods
|
|
536
|
+
within the same scan() call.
|
|
537
|
+
"""
|
|
538
|
+
cache_attr = "_yaml_files_cache"
|
|
539
|
+
cache_root_attr = "_yaml_files_root"
|
|
540
|
+
|
|
541
|
+
cached_root = getattr(self, cache_root_attr, None)
|
|
542
|
+
if cached_root == root:
|
|
543
|
+
cached = getattr(self, cache_attr, None)
|
|
544
|
+
if cached is not None:
|
|
545
|
+
return cached
|
|
546
|
+
|
|
547
|
+
yaml_files: List[Path] = []
|
|
548
|
+
count = 0
|
|
549
|
+
|
|
550
|
+
try:
|
|
551
|
+
for p in walk_project(root, [".yaml", ".yml"]):
|
|
552
|
+
yaml_files.append(p)
|
|
553
|
+
count += 1
|
|
554
|
+
if count >= _MAX_YAML_FILES:
|
|
555
|
+
break
|
|
556
|
+
except Exception:
|
|
557
|
+
pass
|
|
558
|
+
|
|
559
|
+
# Cache on instance
|
|
560
|
+
object.__setattr__(self, cache_attr, yaml_files)
|
|
561
|
+
object.__setattr__(self, cache_root_attr, root)
|
|
562
|
+
|
|
563
|
+
return yaml_files
|
|
564
|
+
|
|
565
|
+
def _safe_read(self, path: Path) -> Optional[str]:
|
|
566
|
+
"""Read a file safely, returning None on failure or if too large."""
|
|
567
|
+
try:
|
|
568
|
+
if not path.is_file():
|
|
569
|
+
return None
|
|
570
|
+
if path.stat().st_size > _MAX_YAML_SIZE:
|
|
571
|
+
return None
|
|
572
|
+
return path.read_text(encoding="utf-8", errors="replace")
|
|
573
|
+
except (OSError, UnicodeDecodeError):
|
|
574
|
+
return None
|
|
575
|
+
|
|
576
|
+
@staticmethod
|
|
577
|
+
def _should_skip_path(path: Path) -> bool:
|
|
578
|
+
"""Check if a path should be skipped (hidden dirs, vendor, node_modules)."""
|
|
579
|
+
parts = path.parts
|
|
580
|
+
for part in parts:
|
|
581
|
+
if part.startswith(".") and part not in (".", ".."):
|
|
582
|
+
return True
|
|
583
|
+
if part in ("node_modules", "vendor", "__pycache__", ".git"):
|
|
584
|
+
return True
|
|
585
|
+
return False
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# Module-level convenience function (matches task verify pattern)
|
|
589
|
+
def scan(root: Path) -> Dict[str, Any]:
|
|
590
|
+
"""Convenience function: instantiate OrchestrationScanner and run scan.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
root: Absolute path to the project root directory.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Dict mapping section names to section data (from ScanResult.sections).
|
|
597
|
+
"""
|
|
598
|
+
scanner = OrchestrationScanner()
|
|
599
|
+
result = scanner.scan(root)
|
|
600
|
+
return result.sections
|