@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
|
@@ -125,29 +125,28 @@ def get_memory_dir(subdir: Optional[str] = None) -> Path:
|
|
|
125
125
|
return memory_dir
|
|
126
126
|
|
|
127
127
|
|
|
128
|
-
def
|
|
128
|
+
def get_events_dir() -> Path:
|
|
129
129
|
"""
|
|
130
|
-
Get the
|
|
130
|
+
Get the events directory, creating it if necessary.
|
|
131
131
|
|
|
132
132
|
Returns:
|
|
133
|
-
Path to .claude/
|
|
133
|
+
Path to .claude/events/
|
|
134
134
|
"""
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return
|
|
135
|
+
events_dir = get_plugin_data_dir() / "events"
|
|
136
|
+
events_dir.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
return events_dir
|
|
138
138
|
|
|
139
139
|
|
|
140
|
-
def
|
|
140
|
+
def get_session_dir() -> Path:
|
|
141
141
|
"""
|
|
142
|
-
Get the
|
|
142
|
+
Get the active session directory, creating it if necessary.
|
|
143
143
|
|
|
144
144
|
Returns:
|
|
145
|
-
Path to
|
|
145
|
+
Path to .claude/session/active/
|
|
146
146
|
"""
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return config_dir
|
|
147
|
+
session_dir = get_plugin_data_dir() / "session" / "active"
|
|
148
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
return session_dir
|
|
151
150
|
|
|
152
151
|
|
|
153
152
|
def clear_path_cache():
|
|
@@ -6,9 +6,11 @@ Ops mode: T3 operations block with nonce for orchestrator agent approval flow.
|
|
|
6
6
|
|
|
7
7
|
Detection order:
|
|
8
8
|
1. plugin-registry.json in plugin data directory
|
|
9
|
-
2.
|
|
10
|
-
3.
|
|
9
|
+
2. NPM package name detection (gaia-ops vs gaia-security)
|
|
10
|
+
3. GAIA_PLUGIN_MODE env var fallback
|
|
11
|
+
4. Default: "security" (most restrictive)
|
|
11
12
|
"""
|
|
13
|
+
from __future__ import annotations
|
|
12
14
|
|
|
13
15
|
import json
|
|
14
16
|
import logging
|
|
@@ -21,6 +23,54 @@ logger = logging.getLogger(__name__)
|
|
|
21
23
|
VALID_MODES = ("security", "ops")
|
|
22
24
|
DEFAULT_MODE = "security"
|
|
23
25
|
|
|
26
|
+
# Map NPM package names to plugin modes
|
|
27
|
+
_NPM_PACKAGE_MODE = {
|
|
28
|
+
"gaia-ops": "ops",
|
|
29
|
+
"gaia-security": "security",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _detect_mode_from_npm_package() -> str | None:
|
|
34
|
+
"""Detect plugin mode from the NPM package name.
|
|
35
|
+
|
|
36
|
+
When installed via npm, this module lives at a path like:
|
|
37
|
+
.../node_modules/@jaguilar87/gaia-ops/hooks/modules/core/plugin_mode.py
|
|
38
|
+
|
|
39
|
+
The package directory name (gaia-ops or gaia-security) determines the mode.
|
|
40
|
+
Also checks .claude/ symlinks as a secondary signal for npm installs.
|
|
41
|
+
|
|
42
|
+
Returns the mode string or None if not detectable.
|
|
43
|
+
"""
|
|
44
|
+
# Primary: check our own file path for node_modules package name
|
|
45
|
+
module_path = Path(__file__).resolve()
|
|
46
|
+
parts = module_path.parts
|
|
47
|
+
for i, part in enumerate(parts):
|
|
48
|
+
if part == "node_modules" and i + 2 < len(parts):
|
|
49
|
+
# Could be @scope/package-name or just package-name
|
|
50
|
+
pkg_name = parts[i + 1]
|
|
51
|
+
if pkg_name.startswith("@") and i + 2 < len(parts):
|
|
52
|
+
pkg_name = parts[i + 2]
|
|
53
|
+
mode = _NPM_PACKAGE_MODE.get(pkg_name)
|
|
54
|
+
if mode:
|
|
55
|
+
logger.debug("Detected mode '%s' from npm package path: %s", mode, pkg_name)
|
|
56
|
+
return mode
|
|
57
|
+
|
|
58
|
+
# Secondary: check if .claude/agents symlink points to a gaia package
|
|
59
|
+
try:
|
|
60
|
+
from .paths import find_claude_dir
|
|
61
|
+
claude_dir = find_claude_dir()
|
|
62
|
+
agents_link = claude_dir / "agents"
|
|
63
|
+
if agents_link.is_symlink():
|
|
64
|
+
target = str(agents_link.resolve())
|
|
65
|
+
for pkg_name, mode in _NPM_PACKAGE_MODE.items():
|
|
66
|
+
if pkg_name in target:
|
|
67
|
+
logger.debug("Detected mode '%s' from .claude/agents symlink target", mode)
|
|
68
|
+
return mode
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
return None
|
|
73
|
+
|
|
24
74
|
|
|
25
75
|
@lru_cache(maxsize=1)
|
|
26
76
|
def get_plugin_mode() -> str:
|
|
@@ -42,12 +92,32 @@ def get_plugin_mode() -> str:
|
|
|
42
92
|
except Exception as e:
|
|
43
93
|
logger.debug("Registry check failed (non-fatal): %s", e)
|
|
44
94
|
|
|
45
|
-
# 2.
|
|
95
|
+
# 2. CLAUDE_PLUGIN_ROOT + plugin.json (--plugin-dir mode)
|
|
96
|
+
plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
|
|
97
|
+
if plugin_root:
|
|
98
|
+
try:
|
|
99
|
+
pjson = Path(plugin_root) / ".claude-plugin" / "plugin.json"
|
|
100
|
+
if pjson.exists():
|
|
101
|
+
pdata = json.loads(pjson.read_text())
|
|
102
|
+
pname = pdata.get("name", "")
|
|
103
|
+
mode = _NPM_PACKAGE_MODE.get(pname)
|
|
104
|
+
if mode:
|
|
105
|
+
logger.debug("Detected mode '%s' from plugin.json name: %s", mode, pname)
|
|
106
|
+
return mode
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.debug("Plugin.json check failed (non-fatal): %s", e)
|
|
109
|
+
|
|
110
|
+
# 3. NPM package name detection
|
|
111
|
+
npm_mode = _detect_mode_from_npm_package()
|
|
112
|
+
if npm_mode:
|
|
113
|
+
return npm_mode
|
|
114
|
+
|
|
115
|
+
# 4. Env var fallback
|
|
46
116
|
mode = os.environ.get("GAIA_PLUGIN_MODE", "").lower()
|
|
47
117
|
if mode in VALID_MODES:
|
|
48
118
|
return mode
|
|
49
119
|
|
|
50
|
-
#
|
|
120
|
+
# 5. Default: security (most restrictive)
|
|
51
121
|
return DEFAULT_MODE
|
|
52
122
|
|
|
53
123
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""First-time plugin setup for SessionStart hook.
|
|
2
2
|
|
|
3
3
|
Detects first run via marker file in CLAUDE_PLUGIN_DATA.
|
|
4
|
-
On first run,
|
|
4
|
+
On first run, merges gaia permissions into .claude/settings.local.json.
|
|
5
5
|
"""
|
|
6
|
+
from __future__ import annotations
|
|
6
7
|
|
|
7
8
|
import json
|
|
8
9
|
import logging
|
|
@@ -16,6 +17,192 @@ logger = logging.getLogger(__name__)
|
|
|
16
17
|
|
|
17
18
|
MARKER_FILE = ".plugin-initialized"
|
|
18
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
|
+
|
|
19
206
|
# Base permissions for security-only mode
|
|
20
207
|
SECURITY_PERMISSIONS = {
|
|
21
208
|
"permissions": {
|
|
@@ -34,7 +221,7 @@ SECURITY_PERMISSIONS = {
|
|
|
34
221
|
"WebSearch",
|
|
35
222
|
"NotebookEdit",
|
|
36
223
|
],
|
|
37
|
-
"deny":
|
|
224
|
+
"deny": _DENY_RULES,
|
|
38
225
|
"ask": [],
|
|
39
226
|
}
|
|
40
227
|
}
|
|
@@ -63,7 +250,7 @@ OPS_PERMISSIONS = {
|
|
|
63
250
|
"Edit(/tmp/*)",
|
|
64
251
|
"Write(/tmp/*)",
|
|
65
252
|
],
|
|
66
|
-
"deny":
|
|
253
|
+
"deny": _DENY_RULES,
|
|
67
254
|
"ask": [],
|
|
68
255
|
}
|
|
69
256
|
}
|
|
@@ -128,18 +315,25 @@ def setup_project_permissions() -> bool:
|
|
|
128
315
|
existing["permissions"]["deny"] = merged_deny
|
|
129
316
|
existing["permissions"].setdefault("ask", [])
|
|
130
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
|
+
|
|
131
323
|
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
132
324
|
settings_path.write_text(json.dumps(existing, indent=2) + "\n")
|
|
133
|
-
logger.info("Merged gaia %s permissions into %s", mode, settings_path)
|
|
325
|
+
logger.info("Merged gaia %s permissions and env into %s", mode, settings_path)
|
|
134
326
|
return True
|
|
135
327
|
|
|
136
328
|
|
|
137
329
|
def ensure_plugin_registry() -> None:
|
|
138
|
-
"""Create plugin-registry.json
|
|
330
|
+
"""Create plugin-registry.json if missing.
|
|
139
331
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
143
337
|
"""
|
|
144
338
|
import os
|
|
145
339
|
data_dir = get_plugin_data_dir()
|
|
@@ -147,37 +341,215 @@ def ensure_plugin_registry() -> None:
|
|
|
147
341
|
if registry_path.exists():
|
|
148
342
|
return
|
|
149
343
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
344
|
+
plugin_name = None
|
|
345
|
+
plugin_version = None
|
|
346
|
+
source = None
|
|
153
347
|
|
|
154
|
-
|
|
155
|
-
|
|
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:
|
|
156
378
|
return
|
|
157
379
|
|
|
158
|
-
plugin_name = parts[-2] # e.g. "gaia-ops" or "gaia-security"
|
|
159
|
-
plugin_version = parts[-1] # e.g. "4.4.0-rc.2"
|
|
160
|
-
|
|
161
380
|
registry = {
|
|
162
|
-
"installed": [{"name": plugin_name, "version": plugin_version}],
|
|
163
|
-
"source":
|
|
381
|
+
"installed": [{"name": plugin_name, "version": plugin_version or "unknown"}],
|
|
382
|
+
"source": source,
|
|
164
383
|
}
|
|
384
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
165
385
|
registry_path.write_text(json.dumps(registry, indent=2) + "\n")
|
|
166
|
-
logger.info("Created plugin-registry.json: %s@%s", plugin_name, plugin_version)
|
|
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
|
|
167
530
|
|
|
168
531
|
|
|
169
|
-
def run_first_time_setup() -> str | None:
|
|
170
|
-
"""Run setup. Returns a reload message if permissions were written.
|
|
171
|
-
|
|
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)
|
|
172
541
|
ensure_plugin_registry()
|
|
173
542
|
reload_needed = setup_project_permissions()
|
|
543
|
+
hooks_changed = setup_project_hooks()
|
|
544
|
+
reload_needed = reload_needed or hooks_changed
|
|
174
545
|
|
|
175
546
|
if not is_first_run():
|
|
176
547
|
if reload_needed:
|
|
177
548
|
return "Permissions updated. Run /reload-plugins to activate."
|
|
178
549
|
return None
|
|
179
550
|
|
|
180
|
-
|
|
551
|
+
if mark_done:
|
|
552
|
+
mark_initialized()
|
|
181
553
|
|
|
182
554
|
if reload_needed:
|
|
183
555
|
mode = get_plugin_mode()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Event context system for cross-session operational event logging."""
|