@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
|
@@ -5,7 +5,8 @@ Primary security gate for all Bash tool invocations. With Bash(*) in the
|
|
|
5
5
|
settings.json allow list, ALL commands reach this hook -- it is the sole
|
|
6
6
|
enforcement layer for dangerous command detection.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Pipeline (ordered by priority):
|
|
9
|
+
0. Indirect execution detection -- bash -c, eval, python -c etc. (T2 approval)
|
|
9
10
|
1. blocked_commands FIRST -- permanently denied patterns (exit 2)
|
|
10
11
|
2. Claude footer stripping -- transparent cleanup via updatedInput
|
|
11
12
|
3. Commit message validation -- conventional commits format
|
|
@@ -30,6 +31,7 @@ from ..security.mutative_verbs import (
|
|
|
30
31
|
from ..security.approval_grants import (
|
|
31
32
|
check_approval_grant,
|
|
32
33
|
confirm_grant,
|
|
34
|
+
find_pending_for_command,
|
|
33
35
|
generate_nonce,
|
|
34
36
|
last_check_found_expired,
|
|
35
37
|
write_pending_approval,
|
|
@@ -63,10 +65,40 @@ class BashValidationResult:
|
|
|
63
65
|
self.suggestions = []
|
|
64
66
|
|
|
65
67
|
|
|
66
|
-
# Patterns for
|
|
68
|
+
# Patterns for AI tool attribution footers (auto-stripped from commits).
|
|
69
|
+
# Covers Claude Code, GitHub Copilot, Aider, Windsurf, and any future
|
|
70
|
+
# tool using the Co-authored-by git trailer convention.
|
|
67
71
|
FORBIDDEN_FOOTER_PATTERNS = [
|
|
68
72
|
r"Generated with\s+Claude Code",
|
|
69
|
-
r"
|
|
73
|
+
r"Generated with\s+\[?Claude Code\]?",
|
|
74
|
+
r"Co-Authored-By:\s+Claude\b",
|
|
75
|
+
r"Co-authored-by:\s+GitHub Copilot\b",
|
|
76
|
+
r"Co-authored-by:\s+aider\b",
|
|
77
|
+
r"Co-authored-by:\s+Windsurf\b",
|
|
78
|
+
r"Co-authored-by:\s+Cursor\b",
|
|
79
|
+
r"Co-authored-by:\s+Codex\b",
|
|
80
|
+
r"Co-authored-by:\s+Gemini\b",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Indirect execution wrappers — commands that execute arbitrary strings.
|
|
85
|
+
# These bypass regex-based command blocking because the real command is
|
|
86
|
+
# hidden inside a string argument. Classified as T2 (requires approval)
|
|
87
|
+
# so the user sees what will actually run.
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
INDIRECT_EXEC_PATTERNS = [
|
|
90
|
+
re.compile(r"^bash\s+-c\s+", re.IGNORECASE),
|
|
91
|
+
re.compile(r"^sh\s+-c\s+", re.IGNORECASE),
|
|
92
|
+
re.compile(r"^zsh\s+-c\s+", re.IGNORECASE),
|
|
93
|
+
re.compile(r"^dash\s+-c\s+", re.IGNORECASE),
|
|
94
|
+
re.compile(r"^\s*eval\s+", re.IGNORECASE),
|
|
95
|
+
re.compile(r"^python3?\s+-c\s+", re.IGNORECASE),
|
|
96
|
+
re.compile(r"^node\s+-e\s+", re.IGNORECASE),
|
|
97
|
+
re.compile(r"^perl\s+-e\s+", re.IGNORECASE),
|
|
98
|
+
re.compile(r"^ruby\s+-e\s+", re.IGNORECASE),
|
|
99
|
+
# Process substitution and heredoc piped to shell
|
|
100
|
+
re.compile(r"^bash\s+<\(", re.IGNORECASE),
|
|
101
|
+
re.compile(r"^sh\s+<\(", re.IGNORECASE),
|
|
70
102
|
]
|
|
71
103
|
|
|
72
104
|
class BashValidator:
|
|
@@ -76,6 +108,91 @@ class BashValidator:
|
|
|
76
108
|
"""Initialize validator."""
|
|
77
109
|
self.shell_parser = get_shell_parser()
|
|
78
110
|
|
|
111
|
+
def _detect_indirect_execution(self, command: str) -> Optional[BashValidationResult]:
|
|
112
|
+
"""Detect indirect execution wrappers that can bypass regex blocking.
|
|
113
|
+
|
|
114
|
+
Commands like 'bash -c "az group delete"' hide the real command inside
|
|
115
|
+
a string. We classify these as T2 (mutative) so they require user
|
|
116
|
+
approval via the nonce workflow, giving the human a chance to inspect
|
|
117
|
+
what will actually run.
|
|
118
|
+
|
|
119
|
+
Returns BashValidationResult if indirect execution detected, else None.
|
|
120
|
+
"""
|
|
121
|
+
for pattern in INDIRECT_EXEC_PATTERNS:
|
|
122
|
+
if pattern.search(command):
|
|
123
|
+
# Also check if the inner payload contains a blocked command.
|
|
124
|
+
# Extract the string argument after the wrapper.
|
|
125
|
+
inner = self._extract_inner_command(command)
|
|
126
|
+
if inner:
|
|
127
|
+
blocked = is_blocked_command(inner)
|
|
128
|
+
if blocked.is_blocked:
|
|
129
|
+
return BashValidationResult(
|
|
130
|
+
allowed=False,
|
|
131
|
+
tier=SecurityTier.T3_BLOCKED,
|
|
132
|
+
reason=(
|
|
133
|
+
f"Indirect execution of blocked command detected: "
|
|
134
|
+
f"{blocked.category} (via wrapper)"
|
|
135
|
+
),
|
|
136
|
+
suggestions=[
|
|
137
|
+
blocked.suggestion or "Run the command directly instead of via a shell wrapper.",
|
|
138
|
+
],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Not blocked but still indirect — route through approval
|
|
142
|
+
logger.info("Indirect execution detected: %s", command[:80])
|
|
143
|
+
result = detect_mutative_command(command)
|
|
144
|
+
if result.is_mutative:
|
|
145
|
+
return None # Already mutative, will be caught by mutative_verbs
|
|
146
|
+
|
|
147
|
+
# For interpreters with inline code analysis (python3 -c),
|
|
148
|
+
# mutative_verbs.py has dedicated pattern scanning that
|
|
149
|
+
# distinguishes safe code (json.dumps, sys.version) from
|
|
150
|
+
# dangerous code (os.system, subprocess.run). If it classified
|
|
151
|
+
# the inline code as safe, trust that analysis and allow it
|
|
152
|
+
# through without forcing an "ask" dialog.
|
|
153
|
+
from ..security.mutative_verbs import _INLINE_CODE_CLIS
|
|
154
|
+
base_cmd = command.strip().split()[0].rsplit("/", 1)[-1].lower()
|
|
155
|
+
if base_cmd in _INLINE_CODE_CLIS:
|
|
156
|
+
logger.info(
|
|
157
|
+
"Inline code classified as safe by pattern scanner: %s",
|
|
158
|
+
command[:80],
|
|
159
|
+
)
|
|
160
|
+
return None # Safe inline code, proceed to normal validation
|
|
161
|
+
|
|
162
|
+
# Shell wrappers (bash -c, eval, etc.) hide the real command
|
|
163
|
+
# in a string — no dedicated scanner exists. Force "ask" so
|
|
164
|
+
# the user can inspect what will actually run.
|
|
165
|
+
hook_block = build_hook_permission_response(
|
|
166
|
+
"ask",
|
|
167
|
+
(
|
|
168
|
+
"Indirect execution detected. The command uses a shell "
|
|
169
|
+
"wrapper (bash -c, eval, etc.) that can bypass "
|
|
170
|
+
"security checks. Please confirm you want to run this."
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
return BashValidationResult(
|
|
174
|
+
allowed=False,
|
|
175
|
+
tier=SecurityTier.T2_DRY_RUN,
|
|
176
|
+
reason="Indirect execution wrapper detected — requires confirmation",
|
|
177
|
+
block_response=hook_block,
|
|
178
|
+
)
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def _extract_inner_command(self, command: str) -> Optional[str]:
|
|
182
|
+
"""Extract the inner command from an indirect execution wrapper.
|
|
183
|
+
|
|
184
|
+
E.g., 'bash -c "az group delete --name foo"' → 'az group delete --name foo'
|
|
185
|
+
"""
|
|
186
|
+
# Match: shell -c "..." or shell -c '...'
|
|
187
|
+
match = re.search(r"""-[ce]\s+(['"])(.*?)\1""", command, re.DOTALL)
|
|
188
|
+
if match:
|
|
189
|
+
return match.group(2).strip()
|
|
190
|
+
# Match: shell -c ... (unquoted, take rest of line)
|
|
191
|
+
match = re.search(r"-[ce]\s+(\S+.*)", command)
|
|
192
|
+
if match:
|
|
193
|
+
return match.group(1).strip()
|
|
194
|
+
return None
|
|
195
|
+
|
|
79
196
|
def _has_operators(self, command: str) -> bool:
|
|
80
197
|
"""Quick check if command has operators (before parsing)."""
|
|
81
198
|
# Fast check for common operators outside quotes
|
|
@@ -84,12 +201,19 @@ class BashValidator:
|
|
|
84
201
|
return False
|
|
85
202
|
return True
|
|
86
203
|
|
|
87
|
-
def validate(
|
|
204
|
+
def validate(
|
|
205
|
+
self,
|
|
206
|
+
command: str,
|
|
207
|
+
is_subagent: bool = False,
|
|
208
|
+
session_id: str = "",
|
|
209
|
+
) -> BashValidationResult:
|
|
88
210
|
"""
|
|
89
211
|
Validate a Bash command.
|
|
90
212
|
|
|
91
213
|
Args:
|
|
92
214
|
command: Command string to validate
|
|
215
|
+
is_subagent: True when running in subagent context
|
|
216
|
+
session_id: Session ID for approval scoping
|
|
93
217
|
|
|
94
218
|
Returns:
|
|
95
219
|
BashValidationResult with validation details
|
|
@@ -103,6 +227,32 @@ class BashValidator:
|
|
|
103
227
|
|
|
104
228
|
command = command.strip()
|
|
105
229
|
|
|
230
|
+
# ================================================================
|
|
231
|
+
# EARLY NORMALIZATION: Strip AI attribution footers before any
|
|
232
|
+
# other processing. This ensures the same normalized command
|
|
233
|
+
# string is used for blocked-command checks, compound parsing,
|
|
234
|
+
# mutative verb detection, pending approval writes, AND pending
|
|
235
|
+
# approval lookups. Without this, write_pending_approval() and
|
|
236
|
+
# find_pending_for_command() could see different strings on the
|
|
237
|
+
# first attempt vs. retry, causing nonce mismatch loops.
|
|
238
|
+
# ================================================================
|
|
239
|
+
command_was_modified = False
|
|
240
|
+
if self._detect_claude_footers(command):
|
|
241
|
+
command = self._strip_claude_footers(command)
|
|
242
|
+
command_was_modified = True
|
|
243
|
+
logger.info("Auto-stripped Claude Code footer from commit command")
|
|
244
|
+
|
|
245
|
+
# ================================================================
|
|
246
|
+
# PRIORITY 0: Indirect execution detection.
|
|
247
|
+
# Commands like "bash -c '...'" or "eval '...'" can hide blocked
|
|
248
|
+
# commands inside string arguments, bypassing regex patterns.
|
|
249
|
+
# Detected wrappers are routed to approval or blocked if the inner
|
|
250
|
+
# payload matches a blocked command.
|
|
251
|
+
# ================================================================
|
|
252
|
+
indirect_result = self._detect_indirect_execution(command)
|
|
253
|
+
if indirect_result is not None:
|
|
254
|
+
return indirect_result
|
|
255
|
+
|
|
106
256
|
# ================================================================
|
|
107
257
|
# PRIORITY 1: Blocked commands check on FULL command (exit 2).
|
|
108
258
|
# This MUST run before any other validator to ensure permanently
|
|
@@ -120,6 +270,7 @@ class BashValidator:
|
|
|
120
270
|
)
|
|
121
271
|
|
|
122
272
|
# Parse compound commands once (reused for blocked-command check and validation dispatch).
|
|
273
|
+
# Runs AFTER footer stripping so components also use the normalized command.
|
|
123
274
|
has_operators = self._has_operators(command)
|
|
124
275
|
parsed_components = None
|
|
125
276
|
if has_operators:
|
|
@@ -136,14 +287,6 @@ class BashValidator:
|
|
|
136
287
|
suggestions=[comp_blocked.suggestion] if comp_blocked.suggestion else [],
|
|
137
288
|
)
|
|
138
289
|
|
|
139
|
-
# Auto-strip forbidden footers from git commits (instead of blocking).
|
|
140
|
-
# Uses updatedInput to transparently clean the command before execution.
|
|
141
|
-
command_was_modified = False
|
|
142
|
-
if self._detect_claude_footers(command):
|
|
143
|
-
command = self._strip_claude_footers(command)
|
|
144
|
-
command_was_modified = True
|
|
145
|
-
logger.info("Auto-stripped Claude Code footer from commit command")
|
|
146
|
-
|
|
147
290
|
# Validate git commit messages (on the potentially cleaned command)
|
|
148
291
|
if "git commit" in command and "-m" in command:
|
|
149
292
|
commit_validation = self._validate_commit_message(command)
|
|
@@ -167,11 +310,17 @@ class BashValidator:
|
|
|
167
310
|
|
|
168
311
|
# Dispatch to single or compound validation using already-parsed components
|
|
169
312
|
if not has_operators:
|
|
170
|
-
result = self._validate_single_command(
|
|
313
|
+
result = self._validate_single_command(
|
|
314
|
+
command, is_subagent=is_subagent, session_id=session_id,
|
|
315
|
+
)
|
|
171
316
|
elif parsed_components is not None and len(parsed_components) > 1:
|
|
172
|
-
result = self._validate_compound_command(
|
|
317
|
+
result = self._validate_compound_command(
|
|
318
|
+
parsed_components, is_subagent=is_subagent, session_id=session_id,
|
|
319
|
+
)
|
|
173
320
|
else:
|
|
174
|
-
result = self._validate_single_command(
|
|
321
|
+
result = self._validate_single_command(
|
|
322
|
+
command, is_subagent=is_subagent, session_id=session_id,
|
|
323
|
+
)
|
|
175
324
|
|
|
176
325
|
# Attach cleaned command for hook to emit via updatedInput.
|
|
177
326
|
# Set regardless of result.allowed so the ask path can include it too.
|
|
@@ -191,99 +340,156 @@ class BashValidator:
|
|
|
191
340
|
|
|
192
341
|
return result
|
|
193
342
|
|
|
194
|
-
def _validate_single_command(
|
|
343
|
+
def _validate_single_command(
|
|
344
|
+
self,
|
|
345
|
+
command: str,
|
|
346
|
+
is_subagent: bool = False,
|
|
347
|
+
session_id: str = "",
|
|
348
|
+
) -> BashValidationResult:
|
|
195
349
|
"""Validate a single command (no operators).
|
|
196
350
|
|
|
197
351
|
Simplified pipeline:
|
|
352
|
+
0. Indirect execution detection (for compound command components)
|
|
198
353
|
1. Mutative verb detection -> block with nonce or allow with grant
|
|
199
354
|
2. GitOps policy validation (for kubectl/helm/flux)
|
|
200
355
|
3. Everything else -> SAFE by elimination
|
|
201
356
|
|
|
357
|
+
Args:
|
|
358
|
+
command: The command to validate.
|
|
359
|
+
is_subagent: True when running in subagent context (generates
|
|
360
|
+
approval_id + deny). False for orchestrator (returns ask).
|
|
361
|
+
session_id: Session ID for pending approval scoping.
|
|
362
|
+
|
|
202
363
|
Note: is_blocked_command() is NOT called here because validate()
|
|
203
364
|
already checks the full command AND each compound component against
|
|
204
365
|
the deny list before dispatching to this method.
|
|
205
366
|
"""
|
|
206
367
|
|
|
368
|
+
# Indirect execution check for compound command components.
|
|
369
|
+
# When validate() splits "cd /tmp && python3 -c '...'" into parts,
|
|
370
|
+
# the python3 -c component needs the same indirect execution gate
|
|
371
|
+
# that the full command gets in validate().
|
|
372
|
+
indirect_result = self._detect_indirect_execution(command)
|
|
373
|
+
if indirect_result is not None:
|
|
374
|
+
return indirect_result
|
|
375
|
+
|
|
207
376
|
# Mutative verb detection
|
|
208
377
|
result = detect_mutative_command(command)
|
|
209
378
|
if result.is_mutative:
|
|
210
379
|
# Check for an active approval grant before blocking.
|
|
211
|
-
grant = check_approval_grant(command)
|
|
380
|
+
grant = check_approval_grant(command, session_id=session_id)
|
|
212
381
|
if grant is not None:
|
|
213
|
-
if
|
|
214
|
-
#
|
|
215
|
-
#
|
|
216
|
-
# (double-barrier security).
|
|
217
|
-
#
|
|
218
|
-
# DO NOT call confirm_grant() here. The native dialog
|
|
219
|
-
# has not been answered yet. If the user declines, the
|
|
220
|
-
# grant must stay unconfirmed. Confirmation happens on
|
|
221
|
-
# the NEXT hook invocation: if we see the same command
|
|
222
|
-
# with an unconfirmed grant again, the native dialog
|
|
223
|
-
# must have succeeded (the command executed and the
|
|
224
|
-
# agent is retrying or a new matching command arrived).
|
|
382
|
+
if grant.confirmed:
|
|
383
|
+
# Already confirmed and consumed -- should not reach
|
|
384
|
+
# here (single-use). But if it does, allow through.
|
|
225
385
|
logger.info(
|
|
226
|
-
"T3 command
|
|
386
|
+
"T3 command allowed via confirmed grant: %s (scope='%s')",
|
|
227
387
|
command[:80], grant.approved_scope,
|
|
228
388
|
)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
389
|
+
return BashValidationResult(
|
|
390
|
+
allowed=True,
|
|
391
|
+
tier=SecurityTier.T3_BLOCKED,
|
|
392
|
+
reason="Grant confirmed",
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
# Grant exists, not yet confirmed -- GAIA approved,
|
|
396
|
+
# let it through. PostToolUse will confirm and consume
|
|
397
|
+
# the grant after successful execution.
|
|
398
|
+
logger.info(
|
|
399
|
+
"T3 command passthrough via active grant: %s (scope='%s')",
|
|
400
|
+
command[:80], grant.approved_scope,
|
|
232
401
|
)
|
|
233
402
|
return BashValidationResult(
|
|
234
|
-
allowed=
|
|
403
|
+
allowed=True,
|
|
235
404
|
tier=SecurityTier.T3_BLOCKED,
|
|
236
|
-
reason="
|
|
237
|
-
block_response=hook_ask,
|
|
405
|
+
reason="Grant active, pending confirmation",
|
|
238
406
|
)
|
|
239
|
-
logger.info(
|
|
240
|
-
"T3 command allowed via approval grant: %s (scope='%s')",
|
|
241
|
-
command[:80], grant.approved_scope,
|
|
242
|
-
)
|
|
243
|
-
return BashValidationResult(
|
|
244
|
-
allowed=True,
|
|
245
|
-
tier=SecurityTier.T3_BLOCKED,
|
|
246
|
-
reason=f"Command allowed via approval grant (tier T3)",
|
|
247
|
-
)
|
|
248
407
|
else:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
"
|
|
256
|
-
"restart the approval workflow.\n\n"
|
|
408
|
+
if is_subagent:
|
|
409
|
+
# Subagent context: check for an existing pending
|
|
410
|
+
# approval first (retry scenario). If found, reuse
|
|
411
|
+
# the same nonce to prevent infinite approval_id
|
|
412
|
+
# generation loops while the user reviews.
|
|
413
|
+
existing_nonce = find_pending_for_command(
|
|
414
|
+
session_id or "", command,
|
|
257
415
|
)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
416
|
+
if existing_nonce:
|
|
417
|
+
approval_id = existing_nonce
|
|
418
|
+
logger.info(
|
|
419
|
+
"Reusing pending approval_id=%s for retry: %s",
|
|
420
|
+
approval_id, command[:80],
|
|
421
|
+
)
|
|
422
|
+
reason = (
|
|
423
|
+
f"[T3_BLOCKED] This command requires user approval.\n"
|
|
424
|
+
f"Do NOT retry this command. Report REVIEW with this approval_id in your json:contract.\n"
|
|
425
|
+
f"Command: {command}\n"
|
|
426
|
+
f"Verb: '{result.verb}' ({result.category})\n"
|
|
427
|
+
f"approval_id: {approval_id}"
|
|
428
|
+
)
|
|
429
|
+
hook_deny = build_hook_permission_response("deny", reason)
|
|
430
|
+
return BashValidationResult(
|
|
431
|
+
allowed=False,
|
|
432
|
+
tier=SecurityTier.T3_BLOCKED,
|
|
433
|
+
reason=f"T3 {result.category.lower()} command: {result.reason}",
|
|
434
|
+
block_response=hook_deny,
|
|
435
|
+
)
|
|
436
|
+
# No existing pending -- generate a new nonce.
|
|
437
|
+
# The ElicitationResult hook will activate the
|
|
438
|
+
# grant when the user approves via AskUserQuestion.
|
|
439
|
+
approval_id = generate_nonce()
|
|
440
|
+
pending_path = write_pending_approval(
|
|
441
|
+
nonce=approval_id,
|
|
442
|
+
command=command,
|
|
443
|
+
danger_verb=result.verb,
|
|
444
|
+
danger_category=result.category,
|
|
445
|
+
session_id=session_id or None,
|
|
446
|
+
)
|
|
447
|
+
if pending_path is None:
|
|
448
|
+
# Persistence failure — fall back to ask
|
|
449
|
+
logger.warning(
|
|
450
|
+
"Failed to persist pending approval for subagent; "
|
|
451
|
+
"falling back to ask: %s",
|
|
452
|
+
command[:80],
|
|
453
|
+
)
|
|
454
|
+
reason = build_pending_approval_unavailable_message()
|
|
455
|
+
hook_ask = build_hook_permission_response("ask", reason)
|
|
456
|
+
return BashValidationResult(
|
|
457
|
+
allowed=False,
|
|
458
|
+
tier=SecurityTier.T3_BLOCKED,
|
|
459
|
+
reason="Pending approval persistence failed",
|
|
460
|
+
block_response=hook_ask,
|
|
461
|
+
)
|
|
462
|
+
reason = (
|
|
463
|
+
f"[T3_BLOCKED] This command requires user approval.\n"
|
|
464
|
+
f"Do NOT retry this command. Report REVIEW with this approval_id in your json:contract.\n"
|
|
465
|
+
f"Command: {command}\n"
|
|
466
|
+
f"Verb: '{result.verb}' ({result.category})\n"
|
|
467
|
+
f"approval_id: {approval_id}"
|
|
271
468
|
)
|
|
469
|
+
hook_deny = build_hook_permission_response("deny", reason)
|
|
272
470
|
return BashValidationResult(
|
|
273
471
|
allowed=False,
|
|
274
472
|
tier=SecurityTier.T3_BLOCKED,
|
|
275
|
-
reason="
|
|
276
|
-
block_response=
|
|
473
|
+
reason=f"T3 {result.category.lower()} command: {result.reason}",
|
|
474
|
+
block_response=hook_deny,
|
|
475
|
+
)
|
|
476
|
+
else:
|
|
477
|
+
# Orchestrator context: route through native 'ask' dialog.
|
|
478
|
+
# The user sees the native permission prompt and approves
|
|
479
|
+
# directly. No approval_id is generated.
|
|
480
|
+
reason = (
|
|
481
|
+
f"[T3_APPROVAL_REQUIRED] {result.category} operation detected.\n"
|
|
482
|
+
f"Command: {command}\n"
|
|
483
|
+
f"Verb: '{result.verb}' ({result.category})\n"
|
|
484
|
+
f"Reason: {result.reason}"
|
|
485
|
+
)
|
|
486
|
+
hook_ask = build_hook_permission_response("ask", reason)
|
|
487
|
+
return BashValidationResult(
|
|
488
|
+
allowed=False,
|
|
489
|
+
tier=SecurityTier.T3_BLOCKED,
|
|
490
|
+
reason=f"Dangerous {result.category.lower()} command: {result.reason}",
|
|
491
|
+
block_response=hook_ask,
|
|
277
492
|
)
|
|
278
|
-
|
|
279
|
-
t3_block = build_t3_block_response(command, result, nonce=nonce)
|
|
280
|
-
hook_block = build_hook_permission_response("deny", expired_prefix + t3_block["message"])
|
|
281
|
-
return BashValidationResult(
|
|
282
|
-
allowed=False,
|
|
283
|
-
tier=SecurityTier.T3_BLOCKED,
|
|
284
|
-
reason=f"Dangerous {result.category.lower()} command: {result.reason}",
|
|
285
|
-
block_response=hook_block,
|
|
286
|
-
)
|
|
287
493
|
|
|
288
494
|
# Check GitOps policy for kubectl/helm/flux commands
|
|
289
495
|
if any(keyword in command for keyword in ("kubectl", "helm", "flux")):
|
|
@@ -303,13 +509,20 @@ class BashValidator:
|
|
|
303
509
|
reason="Safe by elimination (not blocked, not mutative)",
|
|
304
510
|
)
|
|
305
511
|
|
|
306
|
-
def _validate_compound_command(
|
|
512
|
+
def _validate_compound_command(
|
|
513
|
+
self,
|
|
514
|
+
components: List[str],
|
|
515
|
+
is_subagent: bool = False,
|
|
516
|
+
session_id: str = "",
|
|
517
|
+
) -> BashValidationResult:
|
|
307
518
|
"""Validate a compound command (multiple components)."""
|
|
308
519
|
logger.info(f"Compound command detected with {len(components)} components")
|
|
309
520
|
|
|
310
521
|
component_results: List[BashValidationResult] = []
|
|
311
522
|
for i, component in enumerate(components, 1):
|
|
312
|
-
result = self._validate_single_command(
|
|
523
|
+
result = self._validate_single_command(
|
|
524
|
+
component, is_subagent=is_subagent, session_id=session_id,
|
|
525
|
+
)
|
|
313
526
|
|
|
314
527
|
if not result.allowed:
|
|
315
528
|
return BashValidationResult(
|
|
@@ -352,6 +565,8 @@ class BashValidator:
|
|
|
352
565
|
|
|
353
566
|
Removes full lines matching forbidden footer patterns.
|
|
354
567
|
Works on raw command string regardless of quoting/HEREDOC format.
|
|
568
|
+
Preserves trailing quote/paren characters that close the commit
|
|
569
|
+
message (e.g., the closing " in -m "...footer").
|
|
355
570
|
|
|
356
571
|
Args:
|
|
357
572
|
command: Raw command string
|
|
@@ -359,11 +574,15 @@ class BashValidator:
|
|
|
359
574
|
Returns:
|
|
360
575
|
Command with footer lines removed
|
|
361
576
|
"""
|
|
362
|
-
# Remove full lines that contain
|
|
577
|
+
# Remove full lines that contain AI attribution patterns.
|
|
578
|
+
# Each pattern matches the newline + footer content, then uses a
|
|
579
|
+
# lookahead to stop before any trailing quote/paren/bracket
|
|
580
|
+
# sequence that closes the command structure. The captured group
|
|
581
|
+
# is replaced with empty string, leaving the closing chars intact.
|
|
363
582
|
footer_line_patterns = [
|
|
364
|
-
r'\n\s*Co-
|
|
365
|
-
r'\n\s*Generated with\s+\[?Claude Code\]?[^\n]*',
|
|
366
|
-
r'\n\s*🤖\s*Generated with[^\n]*',
|
|
583
|
+
r'\n\s*Co-[Aa]uthored-[Bb]y:\s+(?:Claude|GitHub Copilot|aider|Windsurf|Cursor|Codex|Gemini)[^\n]*?(?=["\')\]]*(?:\n|$))',
|
|
584
|
+
r'\n\s*Generated with\s+\[?Claude Code\]?[^\n]*?(?=["\')\]]*(?:\n|$))',
|
|
585
|
+
r'\n\s*🤖\s*Generated with[^\n]*?(?=["\')\]]*(?:\n|$))',
|
|
367
586
|
]
|
|
368
587
|
for pattern in footer_line_patterns:
|
|
369
588
|
command = re.sub(pattern, '', command, flags=re.IGNORECASE)
|
|
@@ -469,24 +688,21 @@ class BashValidator:
|
|
|
469
688
|
|
|
470
689
|
return None
|
|
471
690
|
|
|
472
|
-
def validate_bash_command(
|
|
691
|
+
def validate_bash_command(
|
|
692
|
+
command: str,
|
|
693
|
+
is_subagent: bool = False,
|
|
694
|
+
session_id: str = "",
|
|
695
|
+
) -> BashValidationResult:
|
|
473
696
|
"""
|
|
474
697
|
Validate a Bash command (convenience function).
|
|
475
698
|
|
|
476
699
|
Args:
|
|
477
700
|
command: Command to validate
|
|
701
|
+
is_subagent: True when running in subagent context
|
|
702
|
+
session_id: Session ID for approval scoping
|
|
478
703
|
|
|
479
704
|
Returns:
|
|
480
705
|
BashValidationResult
|
|
481
706
|
"""
|
|
482
707
|
validator = BashValidator()
|
|
483
|
-
return validator.validate(command)
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
def create_permission_allow_response(reason: str) -> str:
|
|
487
|
-
"""
|
|
488
|
-
Create JSON response to auto-approve a command.
|
|
489
|
-
|
|
490
|
-
This response tells Claude Code to skip the permission check.
|
|
491
|
-
"""
|
|
492
|
-
return json.dumps(build_hook_permission_response("allow", reason))
|
|
708
|
+
return validator.validate(command, is_subagent=is_subagent, session_id=session_id)
|
|
@@ -4,7 +4,7 @@ Task tool validator.
|
|
|
4
4
|
Validates Task tool invocations:
|
|
5
5
|
- Agent existence verification
|
|
6
6
|
- Context provisioning enforcement
|
|
7
|
-
- T3 operation detection for
|
|
7
|
+
- T3 operation detection for user approval workflow
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
import logging
|
|
@@ -18,19 +18,10 @@ from ..security.mutative_verbs import (
|
|
|
18
18
|
MutativeResult,
|
|
19
19
|
CLI_FAMILY_LOOKUP,
|
|
20
20
|
COMMAND_ALIASES,
|
|
21
|
-
CATEGORY_MUTATIVE,
|
|
22
21
|
)
|
|
23
22
|
|
|
24
23
|
logger = logging.getLogger(__name__)
|
|
25
24
|
|
|
26
|
-
# ============================================================================
|
|
27
|
-
# ROUTER INTEGRATION - REMOVED
|
|
28
|
-
# ============================================================================
|
|
29
|
-
# Router suggestion removed for simplicity. Orchestrator should choose correct
|
|
30
|
-
# agent based on improved descriptions in CLAUDE.md. If wrong agent selected,
|
|
31
|
-
# error message shows available agents and orchestrator self-corrects.
|
|
32
|
-
|
|
33
|
-
|
|
34
25
|
# Available agents for Task invocation — both bare and plugin-namespaced forms
|
|
35
26
|
_BASE_AGENTS = [
|
|
36
27
|
"terraform-architect",
|
|
@@ -217,8 +208,8 @@ class TaskValidator:
|
|
|
217
208
|
prompt = parameters.get("prompt", "")
|
|
218
209
|
description = parameters.get("description", "")
|
|
219
210
|
|
|
220
|
-
#
|
|
221
|
-
# runs directly against the original user prompt
|
|
211
|
+
# additionalContext means prompt is never mutated, so T3 detection
|
|
212
|
+
# runs directly against the original user prompt.
|
|
222
213
|
user_task_for_t3_check = prompt
|
|
223
214
|
|
|
224
215
|
logger.info(f"Task tool validation for agent: {agent_name}")
|
|
@@ -229,7 +220,7 @@ class TaskValidator:
|
|
|
229
220
|
error_msg += f"Available agents:\n"
|
|
230
221
|
for agent in sorted(self.available_agents):
|
|
231
222
|
error_msg += f" - {agent}\n"
|
|
232
|
-
error_msg += "\nRefer to
|
|
223
|
+
error_msg += "\nRefer to the Surface Routing Recommendation for agent selection.\n"
|
|
233
224
|
error_msg += f"\nCorrect usage: Task(subagent_type=\"<agent-name>\", ...)"
|
|
234
225
|
|
|
235
226
|
return TaskValidationResult(
|
|
@@ -239,14 +230,11 @@ class TaskValidator:
|
|
|
239
230
|
agent_name=agent_name,
|
|
240
231
|
)
|
|
241
232
|
|
|
242
|
-
#
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
f"Task invocation for {agent_name} without apparent context provisioning. "
|
|
248
|
-
f"Orchestrator should call context_provider.py first (Phase 2)."
|
|
249
|
-
)
|
|
233
|
+
# Context is injected via additionalContext by the adapter, not by
|
|
234
|
+
# mutating the prompt. The validator cannot check additionalContext
|
|
235
|
+
# (it only sees parameters), so we determine context status by agent type.
|
|
236
|
+
# Meta-agents never receive context by design.
|
|
237
|
+
has_context = agent_name not in META_AGENTS
|
|
250
238
|
|
|
251
239
|
# Check for T3 operations (use original user task to avoid false positives from context)
|
|
252
240
|
is_t3 = self._is_t3_operation(user_task_for_t3_check, description)
|
|
@@ -273,14 +261,6 @@ class TaskValidator:
|
|
|
273
261
|
is_t3_operation=is_t3,
|
|
274
262
|
)
|
|
275
263
|
|
|
276
|
-
def _check_context_provisioning(self, prompt: str, agent_name: str) -> bool:
|
|
277
|
-
"""Check if context was properly provisioned."""
|
|
278
|
-
return (
|
|
279
|
-
"# Project Context" in prompt or
|
|
280
|
-
"contract" in prompt.lower() or
|
|
281
|
-
"context_provider.py" in prompt.lower()
|
|
282
|
-
)
|
|
283
|
-
|
|
284
264
|
def _is_t3_operation(self, prompt: str, description: str) -> bool:
|
|
285
265
|
"""Check if this is a T3 (destructive) operation using the verb detector."""
|
|
286
266
|
combined = f"{description} {prompt}"
|