@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,558 @@
|
|
|
1
|
+
"""First-time plugin setup for SessionStart hook.
|
|
2
|
+
|
|
3
|
+
Detects first run via marker file in CLAUDE_PLUGIN_DATA.
|
|
4
|
+
On first run, merges gaia permissions into .claude/settings.local.json.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .paths import get_plugin_data_dir
|
|
14
|
+
from .plugin_mode import get_plugin_mode
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
MARKER_FILE = ".plugin-initialized"
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Deny list — shared across all modes. Aligned with blocked_commands.py
|
|
22
|
+
# (hook-level enforcement) for dual-barrier security. These rules are
|
|
23
|
+
# merged into settings.local.json so Claude Code's native permission system
|
|
24
|
+
# blocks the commands BEFORE they even reach the hook layer.
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
_DENY_RULES = [
|
|
27
|
+
# AWS — networking / data infrastructure (irreversible)
|
|
28
|
+
"Bash(aws ec2 delete-vpc:*)",
|
|
29
|
+
"Bash(aws ec2 delete-subnet:*)",
|
|
30
|
+
"Bash(aws ec2 delete-internet-gateway:*)",
|
|
31
|
+
"Bash(aws ec2 delete-route-table:*)",
|
|
32
|
+
"Bash(aws ec2 delete-route:*)",
|
|
33
|
+
"Bash(aws ec2 terminate-instances:*)",
|
|
34
|
+
"Bash(aws rds delete-db-instance:*)",
|
|
35
|
+
"Bash(aws rds delete-db-cluster:*)",
|
|
36
|
+
"Bash(aws dynamodb delete-table:*)",
|
|
37
|
+
"Bash(aws s3 rb:*)",
|
|
38
|
+
"Bash(aws s3api delete-bucket:*)",
|
|
39
|
+
"Bash(aws elasticache delete-cache-cluster:*)",
|
|
40
|
+
"Bash(aws elasticache delete-replication-group:*)",
|
|
41
|
+
"Bash(aws eks delete-cluster:*)",
|
|
42
|
+
# AWS — KMS / Organizations / Route53
|
|
43
|
+
"Bash(aws kms schedule-key-deletion:*)",
|
|
44
|
+
"Bash(aws organizations delete-organization:*)",
|
|
45
|
+
"Bash(aws route53 delete-hosted-zone:*)",
|
|
46
|
+
# AWS — IAM (mutative but denied at settings level too)
|
|
47
|
+
"Bash(aws iam delete-user:*)",
|
|
48
|
+
"Bash(aws iam delete-role:*)",
|
|
49
|
+
"Bash(aws iam delete-access-key:*)",
|
|
50
|
+
"Bash(aws iam delete-group:*)",
|
|
51
|
+
"Bash(aws iam delete-instance-profile:*)",
|
|
52
|
+
"Bash(aws iam delete-policy:*)",
|
|
53
|
+
"Bash(aws iam delete-role-policy:*)",
|
|
54
|
+
"Bash(aws iam delete-user-policy:*)",
|
|
55
|
+
"Bash(aws iam delete-group-policy:*)",
|
|
56
|
+
"Bash(aws iam detach-user-policy:*)",
|
|
57
|
+
"Bash(aws iam detach-role-policy:*)",
|
|
58
|
+
"Bash(aws iam detach-group-policy:*)",
|
|
59
|
+
"Bash(aws iam remove-user-from-group:*)",
|
|
60
|
+
# AWS — other destructive
|
|
61
|
+
"Bash(aws backup delete:*::*)",
|
|
62
|
+
"Bash(aws cloudformation delete-stack:*)",
|
|
63
|
+
"Bash(aws dynamodb delete-item:*)",
|
|
64
|
+
"Bash(aws ec2 delete-key-pair:*)",
|
|
65
|
+
"Bash(aws ec2 delete-snapshot:*)",
|
|
66
|
+
"Bash(aws ec2 delete-volume:*)",
|
|
67
|
+
"Bash(aws ec2 delete-security-group:*)",
|
|
68
|
+
"Bash(aws ec2 delete-network-interface:*)",
|
|
69
|
+
"Bash(aws lambda delete-function:*)",
|
|
70
|
+
"Bash(aws rds delete-db-cluster-parameter-group:*)",
|
|
71
|
+
"Bash(aws rds delete-db-parameter-group:*)",
|
|
72
|
+
"Bash(aws s3api delete-objects:*)",
|
|
73
|
+
"Bash(aws sns delete-topic:*)",
|
|
74
|
+
"Bash(aws sqs delete-queue:*)",
|
|
75
|
+
"Bash(aws eks delete-nodegroup:*)",
|
|
76
|
+
"Bash(aws eks delete-addon:*)",
|
|
77
|
+
# Azure — resource group / networking / data (irreversible)
|
|
78
|
+
"Bash(az group delete:*)",
|
|
79
|
+
"Bash(az network vnet delete:*)",
|
|
80
|
+
"Bash(az network vnet subnet delete:*)",
|
|
81
|
+
"Bash(az network nsg delete:*)",
|
|
82
|
+
"Bash(az network public-ip delete:*)",
|
|
83
|
+
"Bash(az network application-gateway delete:*)",
|
|
84
|
+
"Bash(az network lb delete:*)",
|
|
85
|
+
"Bash(az network dns zone delete:*)",
|
|
86
|
+
"Bash(az network private-dns zone delete:*)",
|
|
87
|
+
"Bash(az vm delete:*)",
|
|
88
|
+
"Bash(az vmss delete:*)",
|
|
89
|
+
"Bash(az disk delete:*)",
|
|
90
|
+
"Bash(az snapshot delete:*)",
|
|
91
|
+
"Bash(az image delete:*)",
|
|
92
|
+
# Azure — databases / storage
|
|
93
|
+
"Bash(az sql server delete:*)",
|
|
94
|
+
"Bash(az sql db delete:*)",
|
|
95
|
+
"Bash(az cosmosdb delete:*)",
|
|
96
|
+
"Bash(az redis delete:*)",
|
|
97
|
+
"Bash(az storage account delete:*)",
|
|
98
|
+
"Bash(az storage container delete:*)",
|
|
99
|
+
"Bash(az storage blob delete-batch:*)",
|
|
100
|
+
# Azure — AKS / container
|
|
101
|
+
"Bash(az aks delete:*)",
|
|
102
|
+
"Bash(az aks nodepool delete:*)",
|
|
103
|
+
"Bash(az acr delete:*)",
|
|
104
|
+
# Azure — IAM / key vault / functions
|
|
105
|
+
"Bash(az role assignment delete:*)",
|
|
106
|
+
"Bash(az role definition delete:*)",
|
|
107
|
+
"Bash(az ad app delete:*)",
|
|
108
|
+
"Bash(az ad sp delete:*)",
|
|
109
|
+
"Bash(az keyvault delete:*)",
|
|
110
|
+
"Bash(az keyvault key delete:*)",
|
|
111
|
+
"Bash(az keyvault secret delete:*)",
|
|
112
|
+
"Bash(az functionapp delete:*)",
|
|
113
|
+
"Bash(az webapp delete:*)",
|
|
114
|
+
# Azure — messaging / monitoring
|
|
115
|
+
"Bash(az servicebus namespace delete:*)",
|
|
116
|
+
"Bash(az servicebus queue delete:*)",
|
|
117
|
+
"Bash(az servicebus topic delete:*)",
|
|
118
|
+
"Bash(az eventhubs namespace delete:*)",
|
|
119
|
+
"Bash(az eventhubs eventhub delete:*)",
|
|
120
|
+
"Bash(az monitor action-group delete:*)",
|
|
121
|
+
# GCP — project / cluster / database (irreversible)
|
|
122
|
+
"Bash(gcloud projects delete:*)",
|
|
123
|
+
"Bash(gcloud container clusters delete:*)",
|
|
124
|
+
"Bash(gcloud container node-pools delete:*)",
|
|
125
|
+
"Bash(gcloud sql instances delete:*)",
|
|
126
|
+
"Bash(gcloud sql databases delete:*)",
|
|
127
|
+
"Bash(gcloud services disable:*)",
|
|
128
|
+
"Bash(gsutil rb:*)",
|
|
129
|
+
"Bash(gsutil rm -r:*)",
|
|
130
|
+
# GCP — compute / IAM / storage
|
|
131
|
+
"Bash(gcloud compute firewall-rules delete:*)",
|
|
132
|
+
"Bash(gcloud compute instances delete:*)",
|
|
133
|
+
"Bash(gcloud compute networks delete:*)",
|
|
134
|
+
"Bash(gcloud compute disks delete:*)",
|
|
135
|
+
"Bash(gcloud compute images delete:*)",
|
|
136
|
+
"Bash(gcloud compute snapshots delete:*)",
|
|
137
|
+
"Bash(gcloud iam roles delete:*)",
|
|
138
|
+
"Bash(gcloud storage rm:*)",
|
|
139
|
+
# Kubernetes — critical cluster operations
|
|
140
|
+
"Bash(kubectl delete namespace:*)",
|
|
141
|
+
"Bash(kubectl delete node:*)",
|
|
142
|
+
"Bash(kubectl delete cluster:*)",
|
|
143
|
+
"Bash(kubectl delete pv:*)",
|
|
144
|
+
"Bash(kubectl delete persistentvolume:*)",
|
|
145
|
+
"Bash(kubectl delete pvc:*)",
|
|
146
|
+
"Bash(kubectl delete persistentvolumeclaim:*)",
|
|
147
|
+
"Bash(kubectl delete crd:*)",
|
|
148
|
+
"Bash(kubectl delete customresourcedefinition:*)",
|
|
149
|
+
"Bash(kubectl delete mutatingwebhookconfiguration:*)",
|
|
150
|
+
"Bash(kubectl delete validatingwebhookconfiguration:*)",
|
|
151
|
+
"Bash(kubectl delete clusterrole:*)",
|
|
152
|
+
"Bash(kubectl delete clusterrolebinding:*)",
|
|
153
|
+
"Bash(kubectl drain:*)",
|
|
154
|
+
# Flux
|
|
155
|
+
"Bash(flux delete:*)",
|
|
156
|
+
# Git — force push (history rewrite)
|
|
157
|
+
"Bash(git push --force:*)",
|
|
158
|
+
"Bash(git push -f:*)",
|
|
159
|
+
"Bash(git push origin --force:*)",
|
|
160
|
+
"Bash(git push origin -f:*)",
|
|
161
|
+
# Disk / filesystem destruction
|
|
162
|
+
"Bash(dd:*)",
|
|
163
|
+
"Bash(fdisk:*)",
|
|
164
|
+
"Bash(mkfs:*)",
|
|
165
|
+
"Bash(mkfs.ext4:*)",
|
|
166
|
+
"Bash(mkfs.ext3:*)",
|
|
167
|
+
"Bash(mkfs.fat:*)",
|
|
168
|
+
"Bash(mkfs.ntfs:*)",
|
|
169
|
+
# -------------------------------------------------------------------
|
|
170
|
+
# Generic wildcard rules — catch ALL present and future services.
|
|
171
|
+
# These complement the granular rules above; if a new cloud service
|
|
172
|
+
# is added, these patterns block its delete operations automatically.
|
|
173
|
+
# -------------------------------------------------------------------
|
|
174
|
+
# AWS — any "delete-*" subcommand across all services
|
|
175
|
+
"Bash(aws * delete-*:*)",
|
|
176
|
+
"Bash(aws * terminate-*:*)",
|
|
177
|
+
# Azure — any "delete" subcommand across all services
|
|
178
|
+
"Bash(az * delete:*)",
|
|
179
|
+
# GCP — any "delete" subcommand across all services
|
|
180
|
+
"Bash(gcloud * delete:*)",
|
|
181
|
+
"Bash(gsutil rb:*)",
|
|
182
|
+
"Bash(gsutil rm:*)",
|
|
183
|
+
"Bash(gcloud storage rm:*)",
|
|
184
|
+
# Kubernetes — all delete and drain operations
|
|
185
|
+
"Bash(kubectl delete:*)",
|
|
186
|
+
"Bash(kubectl drain:*)",
|
|
187
|
+
# Terraform / Terragrunt — destroy
|
|
188
|
+
"Bash(terraform destroy:*)",
|
|
189
|
+
"Bash(terragrunt destroy:*)",
|
|
190
|
+
"Bash(terragrunt run-all destroy:*)",
|
|
191
|
+
# Helm — uninstall
|
|
192
|
+
"Bash(helm uninstall:*)",
|
|
193
|
+
"Bash(helm delete:*)",
|
|
194
|
+
# Flux — uninstall
|
|
195
|
+
"Bash(flux uninstall:*)",
|
|
196
|
+
# Docker — bulk prune
|
|
197
|
+
"Bash(docker system prune:*)",
|
|
198
|
+
"Bash(docker volume prune:*)",
|
|
199
|
+
# Git — destructive history operations
|
|
200
|
+
"Bash(git reset --hard:*)",
|
|
201
|
+
# Repo deletion
|
|
202
|
+
"Bash(gh repo delete:*)",
|
|
203
|
+
"Bash(glab project delete:*)",
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
# Base permissions for security-only mode
|
|
207
|
+
SECURITY_PERMISSIONS = {
|
|
208
|
+
"permissions": {
|
|
209
|
+
"allow": [
|
|
210
|
+
"Bash(*)",
|
|
211
|
+
"Read",
|
|
212
|
+
"Glob",
|
|
213
|
+
"Grep",
|
|
214
|
+
"BashOutput",
|
|
215
|
+
"ExitPlanMode",
|
|
216
|
+
"KillShell",
|
|
217
|
+
"Skill",
|
|
218
|
+
"SlashCommand",
|
|
219
|
+
"TodoWrite",
|
|
220
|
+
"WebFetch",
|
|
221
|
+
"WebSearch",
|
|
222
|
+
"NotebookEdit",
|
|
223
|
+
],
|
|
224
|
+
"deny": _DENY_RULES,
|
|
225
|
+
"ask": [],
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# Extended permissions for ops mode (adds agent dispatch tools)
|
|
230
|
+
OPS_PERMISSIONS = {
|
|
231
|
+
"permissions": {
|
|
232
|
+
"allow": [
|
|
233
|
+
"Bash(*)",
|
|
234
|
+
"Read",
|
|
235
|
+
"Glob",
|
|
236
|
+
"Grep",
|
|
237
|
+
"BashOutput",
|
|
238
|
+
"ExitPlanMode",
|
|
239
|
+
"KillShell",
|
|
240
|
+
"Skill",
|
|
241
|
+
"SlashCommand",
|
|
242
|
+
"Task",
|
|
243
|
+
"Agent",
|
|
244
|
+
"SendMessage",
|
|
245
|
+
"AskUserQuestion",
|
|
246
|
+
"TodoWrite",
|
|
247
|
+
"WebFetch",
|
|
248
|
+
"WebSearch",
|
|
249
|
+
"NotebookEdit",
|
|
250
|
+
"Edit(/tmp/*)",
|
|
251
|
+
"Write(/tmp/*)",
|
|
252
|
+
],
|
|
253
|
+
"deny": _DENY_RULES,
|
|
254
|
+
"ask": [],
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def is_first_run() -> bool:
|
|
260
|
+
"""Check if this is the first time the plugin runs."""
|
|
261
|
+
marker = get_plugin_data_dir() / MARKER_FILE
|
|
262
|
+
return not marker.exists()
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def mark_initialized() -> None:
|
|
266
|
+
"""Mark the plugin as initialized."""
|
|
267
|
+
marker = get_plugin_data_dir() / MARKER_FILE
|
|
268
|
+
marker.write_text(json.dumps({
|
|
269
|
+
"initialized_at": datetime.now().isoformat(),
|
|
270
|
+
"mode": get_plugin_mode(),
|
|
271
|
+
}))
|
|
272
|
+
logger.info("Plugin marked as initialized: %s", marker)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def setup_project_permissions() -> bool:
|
|
276
|
+
"""Merge gaia permissions into .claude/settings.local.json.
|
|
277
|
+
|
|
278
|
+
Uses settings.local.json (highest project-level precedence) so that
|
|
279
|
+
/reload-plugins picks up changes mid-session without restart.
|
|
280
|
+
Preserves enabledPlugins and any existing user configuration.
|
|
281
|
+
|
|
282
|
+
Returns True if settings were modified (reload needed).
|
|
283
|
+
"""
|
|
284
|
+
claude_dir = Path.cwd() / ".claude"
|
|
285
|
+
settings_path = claude_dir / "settings.local.json"
|
|
286
|
+
|
|
287
|
+
mode = get_plugin_mode()
|
|
288
|
+
our_perms = OPS_PERMISSIONS if mode == "ops" else SECURITY_PERMISSIONS
|
|
289
|
+
our_allow = set(our_perms["permissions"]["allow"])
|
|
290
|
+
our_deny = set(our_perms["permissions"].get("deny", []))
|
|
291
|
+
|
|
292
|
+
# Load existing settings.local.json (has enabledPlugins from install)
|
|
293
|
+
existing = {}
|
|
294
|
+
if settings_path.exists():
|
|
295
|
+
try:
|
|
296
|
+
existing = json.loads(settings_path.read_text())
|
|
297
|
+
except (json.JSONDecodeError, OSError):
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
# Merge permissions — add ours without removing user's
|
|
301
|
+
perms = existing.get("permissions", {})
|
|
302
|
+
current_allow = set(perms.get("allow", []))
|
|
303
|
+
current_deny = set(perms.get("deny", []))
|
|
304
|
+
|
|
305
|
+
merged_allow = sorted(current_allow | our_allow)
|
|
306
|
+
merged_deny = sorted(current_deny | our_deny)
|
|
307
|
+
|
|
308
|
+
if current_allow == set(merged_allow) and current_deny == set(merged_deny):
|
|
309
|
+
logger.info("Project permissions already include gaia rules, skipping")
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
# Update only permissions, preserve everything else (enabledPlugins, etc.)
|
|
313
|
+
existing.setdefault("permissions", {})
|
|
314
|
+
existing["permissions"]["allow"] = merged_allow
|
|
315
|
+
existing["permissions"]["deny"] = merged_deny
|
|
316
|
+
existing["permissions"].setdefault("ask", [])
|
|
317
|
+
|
|
318
|
+
# Add env vars (smart merge: add if not present, don't overwrite)
|
|
319
|
+
env = existing.setdefault("env", {})
|
|
320
|
+
if "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS" not in env:
|
|
321
|
+
env["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] = "1"
|
|
322
|
+
|
|
323
|
+
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
324
|
+
settings_path.write_text(json.dumps(existing, indent=2) + "\n")
|
|
325
|
+
logger.info("Merged gaia %s permissions and env into %s", mode, settings_path)
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def ensure_plugin_registry() -> None:
|
|
330
|
+
"""Create plugin-registry.json if missing.
|
|
331
|
+
|
|
332
|
+
Detection strategies (in order):
|
|
333
|
+
1. CLAUDE_PLUGIN_ROOT env var (plugin marketplace mode):
|
|
334
|
+
Path looks like .../cache/marketplace/gaia-ops/4.4.0-rc.2
|
|
335
|
+
2. NPM package detection: resolve package name and version from
|
|
336
|
+
node_modules path and package.json
|
|
337
|
+
"""
|
|
338
|
+
import os
|
|
339
|
+
data_dir = get_plugin_data_dir()
|
|
340
|
+
registry_path = data_dir / "plugin-registry.json"
|
|
341
|
+
if registry_path.exists():
|
|
342
|
+
return
|
|
343
|
+
|
|
344
|
+
plugin_name = None
|
|
345
|
+
plugin_version = None
|
|
346
|
+
source = None
|
|
347
|
+
|
|
348
|
+
# Strategy 1: CLAUDE_PLUGIN_ROOT (plugin marketplace or --plugin-dir)
|
|
349
|
+
plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
|
|
350
|
+
if plugin_root:
|
|
351
|
+
root_path = Path(plugin_root)
|
|
352
|
+
# First, try to read .claude-plugin/plugin.json (most reliable)
|
|
353
|
+
plugin_json = root_path / ".claude-plugin" / "plugin.json"
|
|
354
|
+
if plugin_json.exists():
|
|
355
|
+
try:
|
|
356
|
+
pdata = json.loads(plugin_json.read_text())
|
|
357
|
+
plugin_name = pdata.get("name")
|
|
358
|
+
plugin_version = pdata.get("version")
|
|
359
|
+
source = "plugin-mode"
|
|
360
|
+
except (json.JSONDecodeError, OSError):
|
|
361
|
+
pass
|
|
362
|
+
# Fallback: parse path (marketplace layout: .../name/version)
|
|
363
|
+
if not plugin_name:
|
|
364
|
+
parts = root_path.parts
|
|
365
|
+
if len(parts) >= 2:
|
|
366
|
+
plugin_name = parts[-2]
|
|
367
|
+
plugin_version = parts[-1]
|
|
368
|
+
source = "plugin-mode"
|
|
369
|
+
|
|
370
|
+
# Strategy 2: NPM package detection
|
|
371
|
+
if not plugin_name:
|
|
372
|
+
npm_info = _detect_npm_package_info()
|
|
373
|
+
if npm_info:
|
|
374
|
+
plugin_name, plugin_version = npm_info
|
|
375
|
+
source = "npm-mode"
|
|
376
|
+
|
|
377
|
+
if not plugin_name:
|
|
378
|
+
return
|
|
379
|
+
|
|
380
|
+
registry = {
|
|
381
|
+
"installed": [{"name": plugin_name, "version": plugin_version or "unknown"}],
|
|
382
|
+
"source": source,
|
|
383
|
+
}
|
|
384
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
385
|
+
registry_path.write_text(json.dumps(registry, indent=2) + "\n")
|
|
386
|
+
logger.info("Created plugin-registry.json: %s@%s (source: %s)", plugin_name, plugin_version, source)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _detect_npm_package_info() -> tuple[str, str | None] | None:
|
|
390
|
+
"""Detect plugin name and version from NPM package path.
|
|
391
|
+
|
|
392
|
+
When installed via npm, this module lives at:
|
|
393
|
+
.../node_modules/@jaguilar87/gaia-ops/hooks/modules/core/plugin_setup.py
|
|
394
|
+
|
|
395
|
+
Returns (plugin_name, version) or None.
|
|
396
|
+
"""
|
|
397
|
+
module_path = Path(__file__).resolve()
|
|
398
|
+
parts = module_path.parts
|
|
399
|
+
|
|
400
|
+
# Find node_modules in path and extract package name
|
|
401
|
+
pkg_name = None
|
|
402
|
+
pkg_root = None
|
|
403
|
+
for i, part in enumerate(parts):
|
|
404
|
+
if part == "node_modules" and i + 1 < len(parts):
|
|
405
|
+
next_part = parts[i + 1]
|
|
406
|
+
if next_part.startswith("@") and i + 2 < len(parts):
|
|
407
|
+
# Scoped package: @scope/name
|
|
408
|
+
pkg_name = parts[i + 2]
|
|
409
|
+
pkg_root = Path(*parts[:i + 3])
|
|
410
|
+
else:
|
|
411
|
+
pkg_name = next_part
|
|
412
|
+
pkg_root = Path(*parts[:i + 2])
|
|
413
|
+
break
|
|
414
|
+
|
|
415
|
+
if not pkg_name or pkg_name not in ("gaia-ops", "gaia-security"):
|
|
416
|
+
return None
|
|
417
|
+
|
|
418
|
+
# Try to read version from package.json
|
|
419
|
+
version = None
|
|
420
|
+
if pkg_root:
|
|
421
|
+
pkg_json = Path("/") / pkg_root / "package.json"
|
|
422
|
+
try:
|
|
423
|
+
if pkg_json.exists():
|
|
424
|
+
data = json.loads(pkg_json.read_text())
|
|
425
|
+
version = data.get("version")
|
|
426
|
+
except Exception:
|
|
427
|
+
pass
|
|
428
|
+
|
|
429
|
+
return (pkg_name, version)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def setup_project_hooks() -> bool:
|
|
433
|
+
"""Merge hooks from hooks.json into .claude/settings.local.json.
|
|
434
|
+
|
|
435
|
+
In npm mode, Claude Code reads hooks from settings files, not hooks.json.
|
|
436
|
+
This converts ${CLAUDE_PLUGIN_ROOT}/hooks/<script> paths to .claude/hooks/<script>
|
|
437
|
+
and merges them into settings.local.json with deduplication by command string.
|
|
438
|
+
|
|
439
|
+
Returns True if settings were modified.
|
|
440
|
+
"""
|
|
441
|
+
import re
|
|
442
|
+
|
|
443
|
+
claude_dir = Path.cwd() / ".claude"
|
|
444
|
+
settings_path = claude_dir / "settings.local.json"
|
|
445
|
+
|
|
446
|
+
# Find hooks.json — try package root (npm) or plugin root
|
|
447
|
+
hooks_json_path = None
|
|
448
|
+
# Strategy 1: relative to this module (npm layout)
|
|
449
|
+
module_dir = Path(__file__).resolve().parent.parent.parent
|
|
450
|
+
candidate = module_dir / "hooks.json"
|
|
451
|
+
if candidate.is_file():
|
|
452
|
+
hooks_json_path = candidate
|
|
453
|
+
else:
|
|
454
|
+
# Strategy 2: .claude/hooks/hooks.json (symlinked)
|
|
455
|
+
candidate2 = claude_dir / "hooks" / "hooks.json"
|
|
456
|
+
if candidate2.is_file():
|
|
457
|
+
hooks_json_path = candidate2
|
|
458
|
+
|
|
459
|
+
if not hooks_json_path:
|
|
460
|
+
logger.info("hooks.json not found, skipping hooks merge")
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
hooks_data = json.loads(hooks_json_path.read_text())
|
|
465
|
+
except (json.JSONDecodeError, OSError):
|
|
466
|
+
logger.warning("hooks.json is invalid, skipping hooks merge")
|
|
467
|
+
return False
|
|
468
|
+
|
|
469
|
+
# Unwrap outer "hooks" key if present
|
|
470
|
+
source_hooks = hooks_data.get("hooks", hooks_data)
|
|
471
|
+
|
|
472
|
+
# Convert ${CLAUDE_PLUGIN_ROOT}/hooks/<script> to .claude/hooks/<script>
|
|
473
|
+
def convert_command(cmd: str) -> str:
|
|
474
|
+
return re.sub(r'\$\{CLAUDE_PLUGIN_ROOT\}/hooks/', '.claude/hooks/', cmd)
|
|
475
|
+
|
|
476
|
+
converted_hooks: dict = {}
|
|
477
|
+
for event, entries in source_hooks.items():
|
|
478
|
+
converted_hooks[event] = []
|
|
479
|
+
for entry in entries:
|
|
480
|
+
new_entry = dict(entry)
|
|
481
|
+
if "hooks" in new_entry:
|
|
482
|
+
new_entry["hooks"] = [
|
|
483
|
+
{**h, "command": convert_command(h["command"])} if "command" in h else h
|
|
484
|
+
for h in new_entry["hooks"]
|
|
485
|
+
]
|
|
486
|
+
converted_hooks[event].append(new_entry)
|
|
487
|
+
|
|
488
|
+
# Load existing settings.local.json
|
|
489
|
+
existing: dict = {}
|
|
490
|
+
if settings_path.exists():
|
|
491
|
+
try:
|
|
492
|
+
existing = json.loads(settings_path.read_text())
|
|
493
|
+
except (json.JSONDecodeError, OSError):
|
|
494
|
+
existing = {}
|
|
495
|
+
|
|
496
|
+
# Smart merge: deduplicate by command string
|
|
497
|
+
existing_hooks = existing.get("hooks", {})
|
|
498
|
+
changed = False
|
|
499
|
+
|
|
500
|
+
for event, new_entries in converted_hooks.items():
|
|
501
|
+
if event not in existing_hooks:
|
|
502
|
+
existing_hooks[event] = new_entries
|
|
503
|
+
changed = True
|
|
504
|
+
continue
|
|
505
|
+
|
|
506
|
+
# Collect existing command strings
|
|
507
|
+
existing_commands: set = set()
|
|
508
|
+
for entry in existing_hooks[event]:
|
|
509
|
+
for h in entry.get("hooks", []):
|
|
510
|
+
if "command" in h:
|
|
511
|
+
existing_commands.add(h["command"])
|
|
512
|
+
|
|
513
|
+
# Add entries whose commands are not already present
|
|
514
|
+
for new_entry in new_entries:
|
|
515
|
+
new_commands = [h["command"] for h in new_entry.get("hooks", []) if "command" in h]
|
|
516
|
+
all_present = len(new_commands) > 0 and all(c in existing_commands for c in new_commands)
|
|
517
|
+
if not all_present:
|
|
518
|
+
existing_hooks[event].append(new_entry)
|
|
519
|
+
changed = True
|
|
520
|
+
|
|
521
|
+
if not changed:
|
|
522
|
+
logger.info("settings.local.json hooks already up to date")
|
|
523
|
+
return False
|
|
524
|
+
|
|
525
|
+
existing["hooks"] = existing_hooks
|
|
526
|
+
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
527
|
+
settings_path.write_text(json.dumps(existing, indent=2) + "\n")
|
|
528
|
+
logger.info("Merged hooks into %s", settings_path)
|
|
529
|
+
return True
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def run_first_time_setup(mark_done: bool = True) -> str | None:
|
|
533
|
+
"""Run setup. Returns a reload message if permissions were written.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
mark_done: If True, mark the plugin as initialized after setup.
|
|
537
|
+
Set to False when the caller wants to defer marking
|
|
538
|
+
(e.g., UserPromptSubmit marks after showing the welcome).
|
|
539
|
+
"""
|
|
540
|
+
# Always ensure registry, permissions, and hooks exist (even on subsequent runs)
|
|
541
|
+
ensure_plugin_registry()
|
|
542
|
+
reload_needed = setup_project_permissions()
|
|
543
|
+
hooks_changed = setup_project_hooks()
|
|
544
|
+
reload_needed = reload_needed or hooks_changed
|
|
545
|
+
|
|
546
|
+
if not is_first_run():
|
|
547
|
+
if reload_needed:
|
|
548
|
+
return "Permissions updated. Run /reload-plugins to activate."
|
|
549
|
+
return None
|
|
550
|
+
|
|
551
|
+
if mark_done:
|
|
552
|
+
mark_initialized()
|
|
553
|
+
|
|
554
|
+
if reload_needed:
|
|
555
|
+
mode = get_plugin_mode()
|
|
556
|
+
return f"GAIA {mode} setup complete. Run /reload-plugins to activate permissions."
|
|
557
|
+
|
|
558
|
+
return None
|