@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
|
@@ -18,7 +18,7 @@ import os
|
|
|
18
18
|
import re
|
|
19
19
|
import time
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
from typing import Any, Dict, Optional
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
22
|
|
|
23
23
|
from .base import HookAdapter
|
|
24
24
|
from .types import (
|
|
@@ -49,7 +49,7 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
49
49
|
- session_id: str
|
|
50
50
|
- tool_name: str (PreToolUse / PostToolUse)
|
|
51
51
|
- tool_input: dict (PreToolUse / PostToolUse)
|
|
52
|
-
-
|
|
52
|
+
- tool_response: dict (PostToolUse only)
|
|
53
53
|
- agent_type: str (SubagentStop only)
|
|
54
54
|
- agent_id: str (SubagentStop only)
|
|
55
55
|
- agent_transcript_path: str (SubagentStop only)
|
|
@@ -177,16 +177,23 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
177
177
|
# ------------------------------------------------------------------ #
|
|
178
178
|
|
|
179
179
|
def format_context_response(self, result: ContextResult) -> HookResponse:
|
|
180
|
-
"""Format a ContextResult for context injection
|
|
180
|
+
"""Format a ContextResult for SubagentStart context injection.
|
|
181
181
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
182
|
+
Claude Code expects SubagentStart hooks to return::
|
|
183
|
+
|
|
184
|
+
{"hookSpecificOutput": {"hookEventName": "SubagentStart",
|
|
185
|
+
"additionalContext": "..."}}
|
|
186
|
+
|
|
187
|
+
The additionalContext string is appended to the subagent's system prompt.
|
|
185
188
|
"""
|
|
186
|
-
|
|
189
|
+
hook_specific: Dict[str, Any] = {
|
|
190
|
+
"hookEventName": "SubagentStart",
|
|
191
|
+
}
|
|
187
192
|
|
|
188
193
|
if result.context_injected and result.additional_context:
|
|
189
|
-
|
|
194
|
+
hook_specific["additionalContext"] = result.additional_context
|
|
195
|
+
|
|
196
|
+
output: Dict[str, Any] = {"hookSpecificOutput": hook_specific}
|
|
190
197
|
|
|
191
198
|
if result.sections_provided:
|
|
192
199
|
output["sections_provided"] = result.sections_provided
|
|
@@ -324,12 +331,12 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
324
331
|
"""
|
|
325
332
|
tool_name = raw.get("tool_name", "")
|
|
326
333
|
tool_input = raw.get("tool_input", {})
|
|
327
|
-
|
|
334
|
+
tool_response = raw.get("tool_response", {})
|
|
328
335
|
session_id = raw.get("session_id", "")
|
|
329
336
|
|
|
330
337
|
command = tool_input.get("command", "")
|
|
331
|
-
output =
|
|
332
|
-
exit_code =
|
|
338
|
+
output = tool_response.get("output", "")
|
|
339
|
+
exit_code = tool_response.get("exit_code", 0)
|
|
333
340
|
|
|
334
341
|
return ToolResult(
|
|
335
342
|
tool_name=tool_name,
|
|
@@ -407,24 +414,11 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
407
414
|
context injection, approval handling, and response formatting.
|
|
408
415
|
"""
|
|
409
416
|
from modules.core.state import create_pre_hook_state, save_hook_state
|
|
410
|
-
from modules.security.approval_constants import (
|
|
411
|
-
NONCE_APPROVAL_PATTERN,
|
|
412
|
-
)
|
|
413
|
-
from modules.security.approval_messages import (
|
|
414
|
-
build_activation_failed_message,
|
|
415
|
-
build_deprecated_approval_message,
|
|
416
|
-
build_invalid_nonce_message,
|
|
417
|
-
)
|
|
418
417
|
from modules.security.approval_grants import (
|
|
419
|
-
activate_pending_approval,
|
|
420
418
|
cleanup_expired_grants,
|
|
421
419
|
)
|
|
422
420
|
from modules.tools.bash_validator import BashValidator
|
|
423
421
|
from modules.tools.task_validator import TaskValidator, AVAILABLE_AGENTS, META_AGENTS
|
|
424
|
-
from modules.security.prompt_validator import classify_resume_prompt
|
|
425
|
-
from modules.context.context_injector import inject_project_context
|
|
426
|
-
from modules.session.session_event_injector import inject_session_events
|
|
427
|
-
|
|
428
422
|
hook_data = event.payload
|
|
429
423
|
tool_name = hook_data.get("tool_name") or ""
|
|
430
424
|
tool_input = hook_data.get("tool_input", {})
|
|
@@ -432,6 +426,28 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
432
426
|
logger.info("Hook invoked: tool=%s, params=%s", tool_name, json.dumps(tool_input)[:200])
|
|
433
427
|
|
|
434
428
|
try:
|
|
429
|
+
# ── Delegate mode gate ─────────────────────────────────
|
|
430
|
+
# Must run before any other logic. When enabled, the
|
|
431
|
+
# orchestrator (main session) is restricted to dispatch-only
|
|
432
|
+
# tools. Subagents are unaffected.
|
|
433
|
+
from modules.orchestrator.delegate_mode import check_delegate_mode
|
|
434
|
+
|
|
435
|
+
dm_result = check_delegate_mode(tool_name, hook_data)
|
|
436
|
+
if dm_result.blocked:
|
|
437
|
+
logger.warning(
|
|
438
|
+
"DELEGATE_MODE denied %s for orchestrator", tool_name,
|
|
439
|
+
)
|
|
440
|
+
return HookResponse(
|
|
441
|
+
output={
|
|
442
|
+
"hookSpecificOutput": {
|
|
443
|
+
"hookEventName": "PreToolUse",
|
|
444
|
+
"permissionDecision": "deny",
|
|
445
|
+
"permissionDecisionReason": dm_result.reason,
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
exit_code=0,
|
|
449
|
+
)
|
|
450
|
+
|
|
435
451
|
# Periodic cleanup of expired approval grants
|
|
436
452
|
cleanup_expired_grants()
|
|
437
453
|
|
|
@@ -441,12 +457,13 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
441
457
|
return HookResponse(output="Error: Invalid parameters", exit_code=2)
|
|
442
458
|
|
|
443
459
|
if tool_name.lower() == "bash":
|
|
444
|
-
return self._adapt_bash(tool_name, tool_input)
|
|
460
|
+
return self._adapt_bash(tool_name, tool_input, hook_data=hook_data)
|
|
445
461
|
elif tool_name.lower() in ("task", "agent"):
|
|
446
462
|
hooks_dir = Path(__file__).parent.parent
|
|
447
463
|
project_agents = [a for a in AVAILABLE_AGENTS if a not in META_AGENTS]
|
|
448
464
|
return self._adapt_task(
|
|
449
465
|
tool_name, tool_input, project_agents, hooks_dir,
|
|
466
|
+
session_id=event.session_id,
|
|
450
467
|
)
|
|
451
468
|
elif tool_name.lower() == "sendmessage":
|
|
452
469
|
return self._adapt_send_message(tool_name, tool_input)
|
|
@@ -461,8 +478,20 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
461
478
|
exit_code=2,
|
|
462
479
|
)
|
|
463
480
|
|
|
464
|
-
def _adapt_bash(
|
|
465
|
-
|
|
481
|
+
def _adapt_bash(
|
|
482
|
+
self,
|
|
483
|
+
tool_name: str,
|
|
484
|
+
parameters: dict,
|
|
485
|
+
hook_data: dict | None = None,
|
|
486
|
+
) -> HookResponse:
|
|
487
|
+
"""Handle Bash tool validation within the adapter.
|
|
488
|
+
|
|
489
|
+
Args:
|
|
490
|
+
tool_name: The tool name ("Bash").
|
|
491
|
+
parameters: The tool_input dict (contains "command").
|
|
492
|
+
hook_data: Full hook event payload -- used to detect subagent
|
|
493
|
+
context via the ``agent_id`` field.
|
|
494
|
+
"""
|
|
466
495
|
from modules.core.state import create_pre_hook_state, save_hook_state
|
|
467
496
|
from modules.tools.bash_validator import BashValidator
|
|
468
497
|
|
|
@@ -470,8 +499,15 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
470
499
|
if not command:
|
|
471
500
|
return HookResponse(output="Error: Bash tool requires a command", exit_code=2)
|
|
472
501
|
|
|
502
|
+
# Detect subagent context: if agent_id is present in the hook event,
|
|
503
|
+
# the command is running inside a subagent (not the orchestrator).
|
|
504
|
+
is_subagent = bool(hook_data and hook_data.get("agent_id"))
|
|
505
|
+
session_id = (hook_data or {}).get("session_id", "")
|
|
506
|
+
|
|
473
507
|
validator = BashValidator()
|
|
474
|
-
result = validator.validate(
|
|
508
|
+
result = validator.validate(
|
|
509
|
+
command, is_subagent=is_subagent, session_id=session_id,
|
|
510
|
+
)
|
|
475
511
|
|
|
476
512
|
if not result.allowed:
|
|
477
513
|
from modules.core.plugin_mode import is_ops_mode
|
|
@@ -542,11 +578,13 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
542
578
|
parameters: dict,
|
|
543
579
|
project_agents: list,
|
|
544
580
|
hooks_dir: Path,
|
|
581
|
+
session_id: str = "",
|
|
545
582
|
) -> HookResponse:
|
|
546
583
|
"""Handle Task/Agent tool validation within the adapter.
|
|
547
584
|
|
|
548
|
-
|
|
549
|
-
|
|
585
|
+
Builds project context and caches it for SubagentStart to forward.
|
|
586
|
+
PreToolUse no longer returns additionalContext directly -- that would
|
|
587
|
+
inject it into the orchestrator instead of the subagent.
|
|
550
588
|
"""
|
|
551
589
|
from modules.core.state import create_pre_hook_state, save_hook_state
|
|
552
590
|
from modules.tools.task_validator import TaskValidator
|
|
@@ -575,18 +613,48 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
575
613
|
|
|
576
614
|
logger.info("ALLOWED Task: %s", result.agent_name)
|
|
577
615
|
|
|
616
|
+
# Cache context for SubagentStart to pick up and forward to the subagent.
|
|
617
|
+
# PreToolUse:Agent additionalContext goes to the orchestrator (wrong target).
|
|
578
618
|
additional = "\n".join(filter(None, [context_text, events_text]))
|
|
619
|
+
|
|
620
|
+
# Fallback: if build_project_context returned None because the
|
|
621
|
+
# orchestrator already embedded context in the prompt (dedup guard),
|
|
622
|
+
# extract the embedded context so SubagentStart can still inject it
|
|
623
|
+
# via additionalContext.
|
|
624
|
+
if not additional:
|
|
625
|
+
prompt = parameters.get("prompt", "")
|
|
626
|
+
marker = "# Project Context"
|
|
627
|
+
if marker in prompt:
|
|
628
|
+
# Extract everything from the marker onwards as context.
|
|
629
|
+
# The orchestrator copied its own injected context into the
|
|
630
|
+
# Agent tool prompt; we forward it to SubagentStart instead.
|
|
631
|
+
idx = prompt.index(marker)
|
|
632
|
+
additional = prompt[idx:]
|
|
633
|
+
logger.info(
|
|
634
|
+
"Extracted embedded context from prompt for caching "
|
|
635
|
+
"(len=%d, agent=%s)",
|
|
636
|
+
len(additional), result.agent_name,
|
|
637
|
+
)
|
|
638
|
+
|
|
579
639
|
if additional:
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
640
|
+
effective_session_id = session_id or "unknown"
|
|
641
|
+
agent_type = result.agent_name or "unknown"
|
|
642
|
+
self._cache_context_for_subagent(effective_session_id, agent_type, additional)
|
|
643
|
+
logger.info(
|
|
644
|
+
"Cached context for SubagentStart: agent=%s, session=%s",
|
|
645
|
+
agent_type, effective_session_id,
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
# Write AGENT_DISPATCH event (non-blocking)
|
|
649
|
+
try:
|
|
650
|
+
from modules.events.event_writer import EventWriter, AGENT_DISPATCH
|
|
651
|
+
prompt = parameters.get("prompt", "")
|
|
652
|
+
EventWriter().write_event(
|
|
653
|
+
AGENT_DISPATCH, "hook", result.agent_name or "unknown",
|
|
654
|
+
f"dispatched for: {prompt[:100]}",
|
|
655
|
+
)
|
|
656
|
+
except Exception:
|
|
657
|
+
pass # Events are non-critical
|
|
590
658
|
|
|
591
659
|
return HookResponse(output={}, exit_code=0)
|
|
592
660
|
|
|
@@ -595,8 +663,9 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
595
663
|
) -> HookResponse:
|
|
596
664
|
"""Handle SendMessage tool validation for agent resumption.
|
|
597
665
|
|
|
598
|
-
Validates agent ID format and message content
|
|
599
|
-
|
|
666
|
+
Validates agent ID format and message content. Does NOT inject
|
|
667
|
+
project context (it's a resume). Nonce relay is no longer processed
|
|
668
|
+
here -- approval grants are activated by the UserPromptSubmit hook.
|
|
600
669
|
"""
|
|
601
670
|
from modules.core.state import create_pre_hook_state, save_hook_state
|
|
602
671
|
|
|
@@ -630,74 +699,19 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
630
699
|
|
|
631
700
|
logger.info("SENDMESSAGE: Resuming agent %s", agent_id)
|
|
632
701
|
|
|
633
|
-
approval_error, has_approval = self._adapt_resume_approval(agent_id, message)
|
|
634
|
-
if approval_error:
|
|
635
|
-
return HookResponse(output=approval_error, exit_code=2)
|
|
636
|
-
|
|
637
702
|
state = create_pre_hook_state(
|
|
638
703
|
tool_name=tool_name,
|
|
639
704
|
command=f"SendMessage:{agent_id}",
|
|
640
705
|
tier="T0",
|
|
641
706
|
allowed=True,
|
|
642
707
|
is_t3=False,
|
|
643
|
-
has_approval=
|
|
708
|
+
has_approval=False,
|
|
644
709
|
)
|
|
645
710
|
save_hook_state(state)
|
|
646
711
|
|
|
647
712
|
logger.info("ALLOWED SendMessage: agent %s - message length: %d", agent_id, len(message))
|
|
648
713
|
return HookResponse(output={}, exit_code=0)
|
|
649
714
|
|
|
650
|
-
def _adapt_resume_approval(
|
|
651
|
-
self, resume_id: str, prompt: str,
|
|
652
|
-
) -> tuple[str | None, bool]:
|
|
653
|
-
"""Process nonce approval indicators for Task resume."""
|
|
654
|
-
from modules.security.approval_constants import NONCE_APPROVAL_PATTERN
|
|
655
|
-
from modules.security.approval_messages import (
|
|
656
|
-
build_activation_failed_message,
|
|
657
|
-
build_deprecated_approval_message,
|
|
658
|
-
build_invalid_nonce_message,
|
|
659
|
-
)
|
|
660
|
-
from modules.security.approval_grants import activate_pending_approval
|
|
661
|
-
from modules.security.prompt_validator import classify_resume_prompt
|
|
662
|
-
|
|
663
|
-
classification = classify_resume_prompt(prompt)
|
|
664
|
-
|
|
665
|
-
if classification == "nonce":
|
|
666
|
-
nonce = NONCE_APPROVAL_PATTERN.search(prompt).group(1)
|
|
667
|
-
activation = activate_pending_approval(nonce)
|
|
668
|
-
status_text = getattr(activation.status, "value", str(activation.status))
|
|
669
|
-
if activation.success:
|
|
670
|
-
grant_path = activation.grant_path
|
|
671
|
-
grant_name = grant_path.name if grant_path else "<unknown>"
|
|
672
|
-
logger.info(
|
|
673
|
-
"Nonce approval activated for resume %s: nonce=%s, file=%s",
|
|
674
|
-
resume_id, nonce, grant_name,
|
|
675
|
-
)
|
|
676
|
-
return None, True
|
|
677
|
-
|
|
678
|
-
logger.warning(
|
|
679
|
-
"Denied resume %s: nonce approval activation failed for nonce=%s "
|
|
680
|
-
"(status=%s, reason=%s)",
|
|
681
|
-
resume_id, nonce, status_text, activation.reason,
|
|
682
|
-
)
|
|
683
|
-
return build_activation_failed_message(nonce, status_text, activation.reason), False
|
|
684
|
-
|
|
685
|
-
if classification == "malformed_nonce":
|
|
686
|
-
logger.warning(
|
|
687
|
-
"Denied resume %s: malformed nonce approval token in prompt='%s'",
|
|
688
|
-
resume_id, prompt[:120],
|
|
689
|
-
)
|
|
690
|
-
return build_invalid_nonce_message(), False
|
|
691
|
-
|
|
692
|
-
if classification == "deprecated":
|
|
693
|
-
logger.warning(
|
|
694
|
-
"Denied resume %s: deprecated legacy approval phrase detected",
|
|
695
|
-
resume_id,
|
|
696
|
-
)
|
|
697
|
-
return build_deprecated_approval_message(), False
|
|
698
|
-
|
|
699
|
-
return None, False
|
|
700
|
-
|
|
701
715
|
@staticmethod
|
|
702
716
|
def _format_blocked_message(result) -> str:
|
|
703
717
|
"""Format blocked command message. Delegates to blocked_message_formatter."""
|
|
@@ -713,25 +727,32 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
713
727
|
|
|
714
728
|
Orchestrates: state retrieval, duration computation, audit logging,
|
|
715
729
|
T3 grant confirmation, critical event detection, session context
|
|
716
|
-
writing, and
|
|
730
|
+
writing, state cleanup, and AskUserQuestion grant activation.
|
|
717
731
|
"""
|
|
718
732
|
from modules.core.state import get_hook_state, clear_hook_state
|
|
719
733
|
from modules.audit.logger import log_execution
|
|
720
734
|
from modules.audit.event_detector import detect_critical_event
|
|
721
735
|
from modules.session.session_context_writer import SessionContextWriter
|
|
722
|
-
from modules.security.approval_grants import check_approval_grant, confirm_grant
|
|
736
|
+
from modules.security.approval_grants import check_approval_grant, confirm_grant, consume_grant
|
|
723
737
|
|
|
724
738
|
hook_data = event.payload
|
|
725
739
|
tool_result_data = self.parse_post_tool_use(hook_data)
|
|
726
740
|
logger.info("Post-hook event: %s", hook_data.get("hook_event_name"))
|
|
727
741
|
|
|
728
|
-
|
|
742
|
+
raw_tool_response = hook_data.get("tool_response", {})
|
|
729
743
|
tool_name = tool_result_data.tool_name
|
|
730
744
|
parameters = hook_data.get("tool_input", {})
|
|
731
745
|
output = tool_result_data.output
|
|
732
|
-
duration =
|
|
746
|
+
duration = raw_tool_response.get("duration_ms", 0) / 1000.0
|
|
733
747
|
success = tool_result_data.exit_code == 0
|
|
734
748
|
|
|
749
|
+
# ------------------------------------------------------------- #
|
|
750
|
+
# AskUserQuestion: check if user approved a pending T3 grant
|
|
751
|
+
# ------------------------------------------------------------- #
|
|
752
|
+
if tool_name == "AskUserQuestion":
|
|
753
|
+
self._handle_ask_user_question_result(hook_data)
|
|
754
|
+
return HookResponse(output={}, exit_code=0)
|
|
755
|
+
|
|
735
756
|
try:
|
|
736
757
|
pre_state = get_hook_state()
|
|
737
758
|
tier = pre_state.tier if pre_state else "unknown"
|
|
@@ -753,12 +774,14 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
753
774
|
# Confirm unconfirmed T3 grants after successful Bash execution
|
|
754
775
|
if tool_name == "Bash" and success:
|
|
755
776
|
command = parameters.get("command", "")
|
|
777
|
+
session_id = hook_data.get("session_id", "")
|
|
756
778
|
if command:
|
|
757
|
-
grant = check_approval_grant(command)
|
|
779
|
+
grant = check_approval_grant(command, session_id=session_id)
|
|
758
780
|
if grant is not None and not grant.confirmed:
|
|
759
|
-
confirm_grant(command)
|
|
781
|
+
confirm_grant(command, session_id=session_id)
|
|
782
|
+
consume_grant(command, session_id=session_id) # Single-use: mark as consumed
|
|
760
783
|
logger.info(
|
|
761
|
-
"T3 grant confirmed post-execution: %s", command[:80],
|
|
784
|
+
"T3 grant confirmed and consumed post-execution: %s", command[:80],
|
|
762
785
|
)
|
|
763
786
|
|
|
764
787
|
events = detect_critical_event(tool_name, parameters, output, success)
|
|
@@ -767,6 +790,20 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
767
790
|
for evt in events:
|
|
768
791
|
writer.update_context(evt.to_dict())
|
|
769
792
|
|
|
793
|
+
# Write COMMAND_EXECUTED event for T2+ Bash commands only (non-blocking)
|
|
794
|
+
if tool_name == "Bash" and tier in ("T2", "T3"):
|
|
795
|
+
try:
|
|
796
|
+
from modules.events.event_writer import EventWriter, COMMAND_EXECUTED
|
|
797
|
+
cmd = parameters.get("command", "")
|
|
798
|
+
EventWriter().write_event(
|
|
799
|
+
COMMAND_EXECUTED, "hook", "",
|
|
800
|
+
f"{'ok' if success else 'error'}: {cmd[:120]}",
|
|
801
|
+
severity="info" if success else "warning",
|
|
802
|
+
meta={"tier": tier},
|
|
803
|
+
)
|
|
804
|
+
except Exception:
|
|
805
|
+
pass # Events are non-critical
|
|
806
|
+
|
|
770
807
|
clear_hook_state()
|
|
771
808
|
logger.debug("Post-hook completed for %s", tool_name)
|
|
772
809
|
|
|
@@ -775,6 +812,77 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
775
812
|
|
|
776
813
|
return HookResponse(output={}, exit_code=0)
|
|
777
814
|
|
|
815
|
+
# ------------------------------------------------------------------ #
|
|
816
|
+
# _handle_ask_user_question_result: grant activation from user answer
|
|
817
|
+
# ------------------------------------------------------------------ #
|
|
818
|
+
|
|
819
|
+
def _handle_ask_user_question_result(self, hook_data: Dict[str, Any]) -> None:
|
|
820
|
+
"""Conditionally activate pending grants based on user's answer.
|
|
821
|
+
|
|
822
|
+
Inspects the answers dict from tool_response (or tool_input as fallback)
|
|
823
|
+
to determine if the user approved. Only activates grants when at least
|
|
824
|
+
one answer value contains "approve" (case-insensitive).
|
|
825
|
+
|
|
826
|
+
Never blocks (no exceptions raised to caller).
|
|
827
|
+
"""
|
|
828
|
+
import json as _json
|
|
829
|
+
from modules.security.approval_grants import (
|
|
830
|
+
activate_grants_for_session,
|
|
831
|
+
get_pending_approvals_for_session,
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
session_id = hook_data.get("session_id", "") or os.environ.get("CLAUDE_SESSION_ID", "")
|
|
835
|
+
|
|
836
|
+
# Debug logging
|
|
837
|
+
tool_response = hook_data.get("tool_response", {})
|
|
838
|
+
logger.info("AskUserQuestion PostToolUse keys: %s", list(hook_data.keys()))
|
|
839
|
+
logger.info("AskUserQuestion tool_response: %s", _json.dumps(tool_response, default=str)[:500])
|
|
840
|
+
|
|
841
|
+
# Extract answers from tool_response first, then tool_input as fallback
|
|
842
|
+
answers = {}
|
|
843
|
+
if isinstance(tool_response, dict):
|
|
844
|
+
answers = tool_response.get("answers", {})
|
|
845
|
+
if not answers and isinstance(hook_data.get("tool_input", {}), dict):
|
|
846
|
+
answers = hook_data.get("tool_input", {}).get("answers", {})
|
|
847
|
+
|
|
848
|
+
if not answers:
|
|
849
|
+
logger.info("AskUserQuestion: no answers found in payload, skipping grant activation")
|
|
850
|
+
return
|
|
851
|
+
|
|
852
|
+
# Check if any answer contains "approve"
|
|
853
|
+
user_approved = any("approve" in str(v).lower() for v in answers.values())
|
|
854
|
+
|
|
855
|
+
if not user_approved:
|
|
856
|
+
logger.info(
|
|
857
|
+
"AskUserQuestion: user did not approve (answers: %s), skipping grant activation",
|
|
858
|
+
{k: v for k, v in answers.items()},
|
|
859
|
+
)
|
|
860
|
+
return
|
|
861
|
+
|
|
862
|
+
# User approved -- activate grants
|
|
863
|
+
logger.info("AskUserQuestion: user approved, activating grants for session %s", session_id[:12])
|
|
864
|
+
|
|
865
|
+
try:
|
|
866
|
+
if not session_id:
|
|
867
|
+
logger.info("AskUserQuestion: no session_id available, skipping grant activation")
|
|
868
|
+
return
|
|
869
|
+
|
|
870
|
+
# Check for pending approvals before activating
|
|
871
|
+
pending = get_pending_approvals_for_session(session_id)
|
|
872
|
+
if not pending:
|
|
873
|
+
logger.info("AskUserQuestion: no pending grants for session %s", session_id)
|
|
874
|
+
return
|
|
875
|
+
|
|
876
|
+
results = activate_grants_for_session(session_id)
|
|
877
|
+
activated = sum(1 for r in results if r.success)
|
|
878
|
+
logger.info(
|
|
879
|
+
"AskUserQuestion activated %d/%d pending grants for session %s",
|
|
880
|
+
activated, len(results), session_id,
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
except Exception as e:
|
|
884
|
+
logger.error("Error in _handle_ask_user_question_result: %s", e, exc_info=True)
|
|
885
|
+
|
|
778
886
|
# ------------------------------------------------------------------ #
|
|
779
887
|
# adapt_subagent_stop: full subagent-stop lifecycle
|
|
780
888
|
# ------------------------------------------------------------------ #
|
|
@@ -792,7 +900,6 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
792
900
|
requires_consolidation_report,
|
|
793
901
|
validate as validate_contract,
|
|
794
902
|
validate_approval_request,
|
|
795
|
-
validate_awaiting_approval_has_nonce,
|
|
796
903
|
validate_verbatim_outputs_consistency,
|
|
797
904
|
)
|
|
798
905
|
from modules.agents.response_contract import (
|
|
@@ -986,6 +1093,24 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
986
1093
|
commands_executed=commands_executed,
|
|
987
1094
|
)
|
|
988
1095
|
|
|
1096
|
+
# Write AGENT_COMPLETE event (non-blocking)
|
|
1097
|
+
try:
|
|
1098
|
+
from modules.events.event_writer import EventWriter, AGENT_COMPLETE
|
|
1099
|
+
_plan = ""
|
|
1100
|
+
if parsed_contract and isinstance(parsed_contract.get("agent_status"), dict):
|
|
1101
|
+
_plan = str(parsed_contract["agent_status"].get("plan_status", ""))
|
|
1102
|
+
_key_outputs = []
|
|
1103
|
+
if parsed_contract and isinstance(parsed_contract.get("evidence_report"), dict):
|
|
1104
|
+
_key_outputs = parsed_contract["evidence_report"].get("key_outputs", [])
|
|
1105
|
+
_summary = "; ".join(str(o) for o in _key_outputs[:2]) if _key_outputs else ""
|
|
1106
|
+
EventWriter().write_event(
|
|
1107
|
+
AGENT_COMPLETE, "hook", agent_type,
|
|
1108
|
+
_plan or "completed",
|
|
1109
|
+
meta={"episode_id": episode_id, "summary": _summary[:200]},
|
|
1110
|
+
)
|
|
1111
|
+
except Exception:
|
|
1112
|
+
pass # Events are non-critical
|
|
1113
|
+
|
|
989
1114
|
contract_attempts = 0
|
|
990
1115
|
if not response_contract.valid:
|
|
991
1116
|
try:
|
|
@@ -1007,19 +1132,11 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
1007
1132
|
)
|
|
1008
1133
|
|
|
1009
1134
|
# ----------------------------------------------------------
|
|
1010
|
-
#
|
|
1011
|
-
# Advisory only -- adds to anomalies but never blocks.
|
|
1135
|
+
# Extract plan_status for downstream checks
|
|
1012
1136
|
# ----------------------------------------------------------
|
|
1013
1137
|
_plan_status = ""
|
|
1014
1138
|
if parsed_contract and isinstance(parsed_contract.get("agent_status"), dict):
|
|
1015
1139
|
_plan_status = str(parsed_contract["agent_status"].get("plan_status", ""))
|
|
1016
|
-
false_pa_check = validate_awaiting_approval_has_nonce(agent_output, _plan_status)
|
|
1017
|
-
if false_pa_check:
|
|
1018
|
-
anomalies.append(false_pa_check)
|
|
1019
|
-
logger.info(
|
|
1020
|
-
"AWAITING_APPROVAL without nonce for %s: %s",
|
|
1021
|
-
agent_type, false_pa_check.get("detail", ""),
|
|
1022
|
-
)
|
|
1023
1140
|
|
|
1024
1141
|
# ----------------------------------------------------------
|
|
1025
1142
|
# Approval request validation
|
|
@@ -1145,9 +1262,16 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
1145
1262
|
QualityResult with quality assessment.
|
|
1146
1263
|
Default: quality_sufficient=True (passthrough until business logic wired).
|
|
1147
1264
|
"""
|
|
1148
|
-
#
|
|
1149
|
-
|
|
1150
|
-
|
|
1265
|
+
# Write SESSION_END event (non-blocking)
|
|
1266
|
+
try:
|
|
1267
|
+
from modules.events.event_writer import EventWriter, SESSION_END
|
|
1268
|
+
stop_reason = raw.get("stop_reason", "unknown")
|
|
1269
|
+
EventWriter().write_event(
|
|
1270
|
+
SESSION_END, "hook", "",
|
|
1271
|
+
f"session ended: {stop_reason}",
|
|
1272
|
+
)
|
|
1273
|
+
except Exception:
|
|
1274
|
+
pass # Events are non-critical
|
|
1151
1275
|
|
|
1152
1276
|
return QualityResult(
|
|
1153
1277
|
quality_sufficient=True,
|
|
@@ -1170,10 +1294,6 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
1170
1294
|
VerificationResult with criteria assessment.
|
|
1171
1295
|
Default: criteria_met=True (passthrough until business logic wired).
|
|
1172
1296
|
"""
|
|
1173
|
-
# Extract task details for future verification logic
|
|
1174
|
-
_task_id = raw.get("task_id", "")
|
|
1175
|
-
_task_output = raw.get("task_output", "")
|
|
1176
|
-
|
|
1177
1297
|
return VerificationResult(
|
|
1178
1298
|
criteria_met=True,
|
|
1179
1299
|
verified_items=[],
|
|
@@ -1181,15 +1301,134 @@ class ClaudeCodeAdapter(HookAdapter):
|
|
|
1181
1301
|
block_completion=False,
|
|
1182
1302
|
)
|
|
1183
1303
|
|
|
1304
|
+
# ------------------------------------------------------------------ #
|
|
1305
|
+
# Context cache: PreToolUse -> SubagentStart bridge
|
|
1306
|
+
# ------------------------------------------------------------------ #
|
|
1307
|
+
|
|
1308
|
+
CONTEXT_CACHE_DIR = Path("/tmp/gaia-context-cache")
|
|
1309
|
+
CONTEXT_CACHE_TTL_SECONDS = 60 # Cache entries older than this are stale
|
|
1310
|
+
|
|
1311
|
+
def _cache_context_for_subagent(
|
|
1312
|
+
self, session_id: str, agent_type: str, context: str,
|
|
1313
|
+
) -> Path:
|
|
1314
|
+
"""Write built context to a cache file for SubagentStart consumption.
|
|
1315
|
+
|
|
1316
|
+
Returns the path to the cache file.
|
|
1317
|
+
"""
|
|
1318
|
+
self.CONTEXT_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
1319
|
+
timestamp = int(time.time() * 1000)
|
|
1320
|
+
cache_file = self.CONTEXT_CACHE_DIR / f"{session_id}-{timestamp}.json"
|
|
1321
|
+
payload = {
|
|
1322
|
+
"context": context,
|
|
1323
|
+
"agent_type": agent_type,
|
|
1324
|
+
"session_id": session_id,
|
|
1325
|
+
"created_at": time.time(),
|
|
1326
|
+
}
|
|
1327
|
+
cache_file.write_text(json.dumps(payload))
|
|
1328
|
+
logger.debug("Context cache written: %s", cache_file)
|
|
1329
|
+
return cache_file
|
|
1330
|
+
|
|
1331
|
+
def _read_cached_context(self, session_id: str) -> Optional[Dict[str, Any]]:
|
|
1332
|
+
"""Read and consume the most recent cached context for a session.
|
|
1333
|
+
|
|
1334
|
+
Finds the newest cache file matching the session_id, reads it,
|
|
1335
|
+
deletes it (one-shot consumption), and cleans up stale entries.
|
|
1336
|
+
|
|
1337
|
+
Returns None if no cache is found.
|
|
1338
|
+
"""
|
|
1339
|
+
if not self.CONTEXT_CACHE_DIR.exists():
|
|
1340
|
+
return None
|
|
1341
|
+
|
|
1342
|
+
# Find all cache files for this session, sorted newest-first
|
|
1343
|
+
candidates: List[Path] = sorted(
|
|
1344
|
+
self.CONTEXT_CACHE_DIR.glob(f"{session_id}-*.json"),
|
|
1345
|
+
key=lambda p: p.stat().st_mtime,
|
|
1346
|
+
reverse=True,
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
if not candidates:
|
|
1350
|
+
# Fallback: try to find the most recent cache file regardless of
|
|
1351
|
+
# session_id, since the orchestrator session_id and the subagent
|
|
1352
|
+
# session_id may differ.
|
|
1353
|
+
all_files = sorted(
|
|
1354
|
+
self.CONTEXT_CACHE_DIR.glob("*.json"),
|
|
1355
|
+
key=lambda p: p.stat().st_mtime,
|
|
1356
|
+
reverse=True,
|
|
1357
|
+
)
|
|
1358
|
+
candidates = all_files
|
|
1359
|
+
|
|
1360
|
+
now = time.time()
|
|
1361
|
+
result = None
|
|
1362
|
+
|
|
1363
|
+
for cache_file in candidates:
|
|
1364
|
+
try:
|
|
1365
|
+
data = json.loads(cache_file.read_text())
|
|
1366
|
+
age = now - data.get("created_at", 0)
|
|
1367
|
+
|
|
1368
|
+
if age > self.CONTEXT_CACHE_TTL_SECONDS:
|
|
1369
|
+
# Stale entry -- clean up
|
|
1370
|
+
cache_file.unlink(missing_ok=True)
|
|
1371
|
+
logger.debug("Cleaned stale context cache: %s (age=%.1fs)", cache_file.name, age)
|
|
1372
|
+
continue
|
|
1373
|
+
|
|
1374
|
+
# Found a valid entry -- consume it
|
|
1375
|
+
result = data
|
|
1376
|
+
cache_file.unlink(missing_ok=True)
|
|
1377
|
+
logger.debug("Consumed context cache: %s (age=%.1fs)", cache_file.name, age)
|
|
1378
|
+
break
|
|
1379
|
+
|
|
1380
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
1381
|
+
logger.warning("Failed to read context cache %s: %s", cache_file, exc)
|
|
1382
|
+
cache_file.unlink(missing_ok=True)
|
|
1383
|
+
continue
|
|
1384
|
+
|
|
1385
|
+
# Clean up any remaining stale files (background hygiene)
|
|
1386
|
+
self._cleanup_stale_cache(now)
|
|
1387
|
+
|
|
1388
|
+
return result
|
|
1389
|
+
|
|
1390
|
+
def _cleanup_stale_cache(self, now: float) -> None:
|
|
1391
|
+
"""Remove cache files older than TTL."""
|
|
1392
|
+
if not self.CONTEXT_CACHE_DIR.exists():
|
|
1393
|
+
return
|
|
1394
|
+
for f in self.CONTEXT_CACHE_DIR.glob("*.json"):
|
|
1395
|
+
try:
|
|
1396
|
+
data = json.loads(f.read_text())
|
|
1397
|
+
if now - data.get("created_at", 0) > self.CONTEXT_CACHE_TTL_SECONDS:
|
|
1398
|
+
f.unlink(missing_ok=True)
|
|
1399
|
+
except (json.JSONDecodeError, OSError):
|
|
1400
|
+
f.unlink(missing_ok=True)
|
|
1401
|
+
|
|
1184
1402
|
# ------------------------------------------------------------------ #
|
|
1185
1403
|
# P2: adapt_subagent_start
|
|
1186
1404
|
# ------------------------------------------------------------------ #
|
|
1187
1405
|
|
|
1188
1406
|
def adapt_subagent_start(self, raw: dict) -> ContextResult:
|
|
1189
|
-
"""Parse SubagentStart event
|
|
1190
|
-
|
|
1191
|
-
|
|
1407
|
+
"""Parse SubagentStart event and forward cached context to the subagent.
|
|
1408
|
+
|
|
1409
|
+
PreToolUse:Agent caches the built project context. This method reads
|
|
1410
|
+
the cache and returns it as additionalContext so Claude Code injects
|
|
1411
|
+
it into the subagent (not the orchestrator).
|
|
1412
|
+
"""
|
|
1413
|
+
session_id = raw.get("session_id", "")
|
|
1192
1414
|
|
|
1415
|
+
cached = self._read_cached_context(session_id)
|
|
1416
|
+
if cached:
|
|
1417
|
+
logger.info(
|
|
1418
|
+
"SubagentStart: forwarding cached context for agent=%s (session=%s)",
|
|
1419
|
+
cached.get("agent_type", "unknown"),
|
|
1420
|
+
session_id,
|
|
1421
|
+
)
|
|
1422
|
+
return ContextResult(
|
|
1423
|
+
context_injected=True,
|
|
1424
|
+
additional_context=cached["context"],
|
|
1425
|
+
sections_provided=[],
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
logger.info(
|
|
1429
|
+
"SubagentStart: no cached context found for session=%s (passthrough)",
|
|
1430
|
+
session_id,
|
|
1431
|
+
)
|
|
1193
1432
|
return ContextResult(
|
|
1194
1433
|
context_injected=False,
|
|
1195
1434
|
additional_context=None,
|