@jaguilar87/gaia-ops 4.4.0 → 4.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +12 -3
- package/ARCHITECTURE.md +9 -8
- package/CHANGELOG.md +34 -0
- package/README.md +43 -11
- package/agents/terraform-architect.md +1 -1
- package/bin/README.md +2 -2
- package/bin/gaia-doctor.js +18 -5
- package/bin/gaia-history.js +0 -1
- package/bin/gaia-metrics.js +2 -2
- package/bin/gaia-scan.py +23 -1
- package/bin/gaia-update.js +346 -54
- package/bin/pre-publish-validate.js +33 -10
- package/commands/gaia.md +37 -0
- package/config/README.md +3 -9
- package/config/context-contracts.json +47 -15
- package/config/surface-routing.json +9 -1
- package/dist/gaia-ops/.claude-plugin/plugin.json +22 -0
- package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
- package/dist/gaia-ops/agents/devops-developer.md +57 -0
- package/dist/gaia-ops/agents/gaia-system.md +58 -0
- package/dist/gaia-ops/agents/gitops-operator.md +60 -0
- package/dist/gaia-ops/agents/speckit-planner.md +71 -0
- package/dist/gaia-ops/agents/terraform-architect.md +60 -0
- package/dist/gaia-ops/commands/gaia.md +37 -0
- package/dist/gaia-ops/config/README.md +58 -0
- package/dist/gaia-ops/config/cloud/aws.json +140 -0
- package/dist/gaia-ops/config/cloud/gcp.json +145 -0
- package/dist/gaia-ops/config/context-contracts.json +131 -0
- package/dist/gaia-ops/config/git_standards.json +72 -0
- package/dist/gaia-ops/config/surface-routing.json +197 -0
- package/dist/gaia-ops/config/universal-rules.json +10 -0
- package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-ops/hooks/adapters/base.py +219 -0
- package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
- package/dist/gaia-ops/hooks/adapters/claude_code.py +1477 -0
- package/dist/gaia-ops/hooks/adapters/types.py +194 -0
- package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
- package/dist/gaia-ops/hooks/hooks.json +126 -0
- package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
- package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +124 -0
- package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +576 -0
- package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +215 -0
- package/dist/gaia-ops/hooks/modules/context/context_cache.py +129 -0
- package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +427 -0
- package/dist/gaia-ops/hooks/modules/context/context_writer.py +518 -0
- package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +558 -0
- package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
- package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-ops/hooks/modules/identity/__init__.py +0 -0
- package/dist/gaia-ops/hooks/modules/identity/identity_provider.py +21 -0
- package/dist/gaia-ops/hooks/modules/identity/ops_identity.py +34 -0
- package/dist/gaia-ops/hooks/modules/identity/security_identity.py +10 -0
- package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +227 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-ops/hooks/modules/security/__init__.py +89 -0
- package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-ops/hooks/modules/security/approval_grants.py +912 -0
- package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +153 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +584 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +86 -0
- package/dist/gaia-ops/hooks/modules/security/command_semantics.py +130 -0
- package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +850 -0
- package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +158 -0
- package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-ops/hooks/modules/tools/__init__.py +25 -0
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +708 -0
- package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +181 -0
- package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-ops/hooks/modules/tools/task_validator.py +283 -0
- package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-ops/hooks/post_compact.py +43 -0
- package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
- package/dist/gaia-ops/hooks/pre_tool_use.py +383 -0
- package/dist/gaia-ops/hooks/session_start.py +69 -0
- package/dist/gaia-ops/hooks/stop_hook.py +69 -0
- package/dist/gaia-ops/hooks/subagent_start.py +71 -0
- package/dist/gaia-ops/hooks/subagent_stop.py +288 -0
- package/dist/gaia-ops/hooks/task_completed.py +70 -0
- package/dist/gaia-ops/hooks/user_prompt_submit.py +177 -0
- package/dist/gaia-ops/settings.json +72 -0
- package/dist/gaia-ops/skills/README.md +109 -0
- package/dist/gaia-ops/skills/agent-protocol/SKILL.md +105 -0
- package/dist/gaia-ops/skills/agent-protocol/examples.md +170 -0
- package/dist/gaia-ops/skills/agent-response/SKILL.md +53 -0
- package/dist/gaia-ops/skills/approval/SKILL.md +85 -0
- package/dist/gaia-ops/skills/approval/examples.md +140 -0
- package/dist/gaia-ops/skills/approval/reference.md +57 -0
- package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
- package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
- package/dist/gaia-ops/skills/context-updater/SKILL.md +76 -0
- package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
- package/dist/gaia-ops/skills/developer-patterns/SKILL.md +93 -0
- package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
- package/dist/gaia-ops/skills/execution/SKILL.md +66 -0
- package/dist/gaia-ops/skills/fast-queries/SKILL.md +47 -0
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +92 -0
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +22 -0
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +48 -0
- package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +73 -0
- package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
- package/dist/gaia-ops/skills/investigation/SKILL.md +77 -0
- package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +64 -0
- package/dist/gaia-ops/skills/reference.md +134 -0
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +61 -0
- package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
- package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
- package/dist/gaia-ops/skills/skill-creation/SKILL.md +119 -0
- package/dist/gaia-ops/skills/specification/SKILL.md +186 -0
- package/dist/gaia-ops/skills/speckit-workflow/SKILL.md +165 -0
- package/dist/gaia-ops/skills/speckit-workflow/reference.md +117 -0
- package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +63 -0
- package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
- package/dist/gaia-ops/speckit/README.md +516 -0
- package/dist/gaia-ops/speckit/scripts/.gitkeep +0 -0
- package/dist/gaia-ops/speckit/templates/adr-template.md +118 -0
- package/dist/gaia-ops/speckit/templates/agent-file-template.md +23 -0
- package/dist/gaia-ops/speckit/templates/plan-template.md +227 -0
- package/dist/gaia-ops/speckit/templates/spec-template.md +140 -0
- package/dist/gaia-ops/speckit/templates/tasks-template.md +257 -0
- package/dist/gaia-ops/tools/context/README.md +132 -0
- package/dist/gaia-ops/tools/context/__init__.py +42 -0
- package/dist/gaia-ops/tools/context/_paths.py +20 -0
- package/dist/gaia-ops/tools/context/context_provider.py +476 -0
- package/dist/gaia-ops/tools/context/context_section_reader.py +330 -0
- package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
- package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
- package/dist/gaia-ops/tools/context/surface_router.py +278 -0
- package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
- package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
- package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
- package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
- package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
- package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
- package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
- package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
- package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
- package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
- package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
- package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
- package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
- package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +262 -0
- package/dist/gaia-ops/tools/memory/README.md +0 -0
- package/dist/gaia-ops/tools/memory/__init__.py +20 -0
- package/dist/gaia-ops/tools/memory/episodic.py +1196 -0
- package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
- package/dist/gaia-ops/tools/review/__init__.py +1 -0
- package/dist/gaia-ops/tools/review/review_engine.py +157 -0
- package/dist/gaia-ops/tools/scan/__init__.py +35 -0
- package/dist/gaia-ops/tools/scan/config.py +247 -0
- package/dist/gaia-ops/tools/scan/merge.py +212 -0
- package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
- package/dist/gaia-ops/tools/scan/registry.py +127 -0
- package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
- package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
- package/dist/gaia-ops/tools/scan/scanners/environment.py +324 -0
- package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
- package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
- package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
- package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
- package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
- package/dist/gaia-ops/tools/scan/setup.py +753 -0
- package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
- package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
- package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
- package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
- package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
- package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
- package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
- package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
- package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
- package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
- package/dist/gaia-ops/tools/scan/ui.py +624 -0
- package/dist/gaia-ops/tools/scan/verify.py +266 -0
- package/dist/gaia-ops/tools/scan/walk.py +118 -0
- package/dist/gaia-ops/tools/scan/workspace.py +85 -0
- package/dist/gaia-ops/tools/validation/README.md +244 -0
- package/dist/gaia-ops/tools/validation/__init__.py +17 -0
- package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
- package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
- package/dist/gaia-security/.claude-plugin/plugin.json +22 -0
- package/dist/gaia-security/config/universal-rules.json +10 -0
- package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
- package/dist/gaia-security/hooks/adapters/base.py +219 -0
- package/dist/gaia-security/hooks/adapters/channel.py +17 -0
- package/dist/gaia-security/hooks/adapters/claude_code.py +1477 -0
- package/dist/gaia-security/hooks/adapters/types.py +194 -0
- package/dist/gaia-security/hooks/adapters/utils.py +25 -0
- package/dist/gaia-security/hooks/hooks.json +57 -0
- package/dist/gaia-security/hooks/modules/__init__.py +15 -0
- package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
- package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
- package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
- package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +124 -0
- package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
- package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
- package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
- package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
- package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +576 -0
- package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
- package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
- package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +215 -0
- package/dist/gaia-security/hooks/modules/context/context_cache.py +129 -0
- package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-security/hooks/modules/context/context_injector.py +427 -0
- package/dist/gaia-security/hooks/modules/context/context_writer.py +518 -0
- package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
- package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
- package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
- package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
- package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
- package/dist/gaia-security/hooks/modules/core/plugin_setup.py +558 -0
- package/dist/gaia-security/hooks/modules/core/state.py +179 -0
- package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
- package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
- package/dist/gaia-security/hooks/modules/identity/__init__.py +0 -0
- package/dist/gaia-security/hooks/modules/identity/identity_provider.py +21 -0
- package/dist/gaia-security/hooks/modules/identity/ops_identity.py +34 -0
- package/dist/gaia-security/hooks/modules/identity/security_identity.py +10 -0
- package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/memory/episode_writer.py +227 -0
- package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
- package/dist/gaia-security/hooks/modules/security/__init__.py +89 -0
- package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
- package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
- package/dist/gaia-security/hooks/modules/security/approval_grants.py +912 -0
- package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-security/hooks/modules/security/approval_scopes.py +153 -0
- package/dist/gaia-security/hooks/modules/security/blocked_commands.py +584 -0
- package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +86 -0
- package/dist/gaia-security/hooks/modules/security/command_semantics.py +130 -0
- package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +850 -0
- package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
- package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
- package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
- package/dist/gaia-security/hooks/modules/session/session_event_injector.py +158 -0
- package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-security/hooks/modules/tools/__init__.py +25 -0
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +708 -0
- package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +181 -0
- package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
- package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
- package/dist/gaia-security/hooks/modules/tools/task_validator.py +283 -0
- package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
- package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
- package/dist/gaia-security/hooks/post_tool_use.py +54 -0
- package/dist/gaia-security/hooks/pre_tool_use.py +383 -0
- package/dist/gaia-security/hooks/session_start.py +69 -0
- package/dist/gaia-security/hooks/stop_hook.py +69 -0
- package/dist/gaia-security/hooks/user_prompt_submit.py +177 -0
- package/dist/gaia-security/settings.json +58 -0
- package/git-hooks/commit-msg +41 -0
- package/hooks/README.md +8 -6
- package/hooks/adapters/channel.py +0 -25
- package/hooks/adapters/claude_code.py +364 -125
- package/hooks/elicitation_result.py +132 -0
- package/hooks/hooks.json +10 -1
- package/hooks/modules/README.md +3 -2
- package/hooks/modules/agents/contract_validator.py +3 -51
- package/hooks/modules/agents/response_contract.py +4 -8
- package/hooks/modules/agents/transcript_reader.py +4 -5
- package/hooks/modules/audit/__init__.py +4 -6
- package/hooks/modules/audit/event_detector.py +0 -2
- package/hooks/modules/audit/metrics.py +108 -187
- package/hooks/modules/audit/workflow_auditor.py +0 -4
- package/hooks/modules/audit/workflow_recorder.py +0 -5
- package/hooks/modules/context/compact_context_builder.py +1 -0
- package/hooks/modules/context/context_cache.py +129 -0
- package/hooks/modules/context/context_injector.py +18 -40
- package/hooks/modules/context/context_writer.py +1 -25
- package/hooks/modules/context/contracts_loader.py +7 -10
- package/hooks/modules/core/hook_entry.py +1 -0
- package/hooks/modules/core/paths.py +12 -13
- package/hooks/modules/core/plugin_mode.py +74 -4
- package/hooks/modules/core/plugin_setup.py +395 -23
- package/hooks/modules/events/__init__.py +1 -0
- package/hooks/modules/events/event_writer.py +210 -0
- package/hooks/modules/identity/ops_identity.py +18 -27
- package/hooks/modules/memory/episode_writer.py +1 -6
- package/hooks/modules/orchestrator/__init__.py +1 -0
- package/hooks/modules/orchestrator/delegate_mode.py +128 -0
- package/hooks/modules/security/__init__.py +2 -4
- package/hooks/modules/security/approval_constants.py +5 -1
- package/hooks/modules/security/approval_grants.py +189 -6
- package/hooks/modules/security/approval_messages.py +9 -21
- package/hooks/modules/security/blocked_commands.py +98 -34
- package/hooks/modules/security/command_semantics.py +0 -4
- package/hooks/modules/security/gitops_validator.py +1 -11
- package/hooks/modules/security/mutative_verbs.py +179 -38
- package/hooks/modules/security/tiers.py +1 -19
- package/hooks/modules/session/session_event_injector.py +1 -25
- package/hooks/modules/tools/bash_validator.py +310 -94
- package/hooks/modules/tools/shell_parser.py +0 -1
- package/hooks/modules/tools/task_validator.py +9 -29
- package/hooks/post_tool_use.py +0 -72
- package/hooks/pre_tool_use.py +42 -102
- package/hooks/session_start.py +4 -2
- package/hooks/subagent_start.py +6 -2
- package/hooks/subagent_stop.py +1 -13
- package/hooks/user_prompt_submit.py +119 -37
- package/index.js +1 -1
- package/package.json +5 -3
- package/skills/README.md +3 -5
- package/skills/agent-protocol/SKILL.md +17 -16
- package/skills/agent-protocol/examples.md +6 -6
- package/skills/agent-response/SKILL.md +11 -14
- package/skills/approval/SKILL.md +28 -13
- package/skills/approval/reference.md +2 -2
- package/skills/execution/SKILL.md +1 -1
- package/skills/gaia-patterns/SKILL.md +2 -3
- package/skills/orchestrator-approval/SKILL.md +22 -50
- package/skills/security-tiers/SKILL.md +1 -1
- package/templates/README.md +9 -9
- package/templates/managed-settings.template.json +43 -0
- package/tools/gaia_simulator/runner.py +34 -1
- package/tools/scan/orchestrator.py +13 -0
- package/tools/scan/scanners/base.py +8 -0
- package/tools/scan/scanners/git.py +78 -0
- package/tools/scan/scanners/infrastructure.py +65 -0
- package/tools/scan/scanners/stack.py +110 -0
- package/tools/scan/setup.py +120 -13
- package/tools/scan/workspace.py +85 -0
- package/config/context-contracts.aws.json +0 -42
- package/config/context-contracts.gcp.json +0 -39
- package/skills/project-dispatch/SKILL.md +0 -34
- package/templates/settings.template.json +0 -226
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Episodic memory capture for workflow episodes.
|
|
3
|
+
|
|
4
|
+
Renamed from episode_capture.py. Absorbs get_session_events() from
|
|
5
|
+
session_state.py directly into this module.
|
|
6
|
+
|
|
7
|
+
Provides:
|
|
8
|
+
- write(): Store workflow as episodic memory
|
|
9
|
+
- get_session_events(): Read context.json, categorize events
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ============================================================================
|
|
21
|
+
# Session events (absorbed from session_state.py)
|
|
22
|
+
# ============================================================================
|
|
23
|
+
|
|
24
|
+
def get_session_events() -> Dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Get critical events from active session context.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Dict with categorized session events (commits, pushes, file_mods, speckit)
|
|
30
|
+
"""
|
|
31
|
+
context_path = Path(".claude/session/active/context.json")
|
|
32
|
+
|
|
33
|
+
if not context_path.exists():
|
|
34
|
+
logger.debug("No session context found")
|
|
35
|
+
return {}
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
with open(context_path, "r") as f:
|
|
39
|
+
context = json.load(f)
|
|
40
|
+
|
|
41
|
+
critical_events = context.get("critical_events", [])
|
|
42
|
+
|
|
43
|
+
if not critical_events:
|
|
44
|
+
return {}
|
|
45
|
+
|
|
46
|
+
commits = [
|
|
47
|
+
{
|
|
48
|
+
"hash": e.get("commit_hash", ""),
|
|
49
|
+
"message": e.get("commit_message", ""),
|
|
50
|
+
"timestamp": e.get("timestamp", "")
|
|
51
|
+
}
|
|
52
|
+
for e in critical_events
|
|
53
|
+
if e.get("event_type") == "git_commit" and e.get("commit_hash")
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
pushes = [
|
|
57
|
+
{
|
|
58
|
+
"branch": e.get("branch", ""),
|
|
59
|
+
"timestamp": e.get("timestamp", "")
|
|
60
|
+
}
|
|
61
|
+
for e in critical_events
|
|
62
|
+
if e.get("event_type") == "git_push" and e.get("branch")
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
file_mods = [
|
|
66
|
+
{
|
|
67
|
+
"count": e.get("modification_count", 0),
|
|
68
|
+
"timestamp": e.get("timestamp", "")
|
|
69
|
+
}
|
|
70
|
+
for e in critical_events
|
|
71
|
+
if e.get("event_type") == "file_modifications"
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
speckit = [
|
|
75
|
+
{
|
|
76
|
+
"command": e.get("command", ""),
|
|
77
|
+
"timestamp": e.get("timestamp", "")
|
|
78
|
+
}
|
|
79
|
+
for e in critical_events
|
|
80
|
+
if e.get("event_type") == "speckit_milestone"
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
result = {}
|
|
84
|
+
if commits:
|
|
85
|
+
result["git_commits"] = commits
|
|
86
|
+
if pushes:
|
|
87
|
+
result["git_pushes"] = pushes
|
|
88
|
+
if file_mods:
|
|
89
|
+
result["file_modifications"] = file_mods
|
|
90
|
+
if speckit:
|
|
91
|
+
result["speckit_milestones"] = speckit
|
|
92
|
+
|
|
93
|
+
if result:
|
|
94
|
+
logger.info(f"Found {len(critical_events)} session events")
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.warning(f"Failed to read session events: {e}")
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ============================================================================
|
|
104
|
+
# Episodic memory capture
|
|
105
|
+
# ============================================================================
|
|
106
|
+
|
|
107
|
+
def write(
|
|
108
|
+
metrics: Dict[str, Any],
|
|
109
|
+
anomalies: Optional[List[Dict[str, str]]] = None,
|
|
110
|
+
commands_executed: Optional[List[str]] = None,
|
|
111
|
+
) -> Optional[str]:
|
|
112
|
+
"""
|
|
113
|
+
Capture workflow as episodic memory.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
metrics: Subagent metrics from workflow (includes plan_status, tier, task description)
|
|
117
|
+
anomalies: Detected anomalies from audit(), stored in episode context
|
|
118
|
+
commands_executed: List of commands extracted from EVIDENCE_REPORT
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Episode ID if stored, None otherwise
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
import importlib.util
|
|
125
|
+
|
|
126
|
+
candidates = [
|
|
127
|
+
Path(__file__).parent.parent.parent.parent / "tools" / "memory" / "episodic.py",
|
|
128
|
+
Path(".claude/tools/memory/episodic.py"),
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
episodic_module = None
|
|
132
|
+
for path in candidates:
|
|
133
|
+
if path.exists():
|
|
134
|
+
try:
|
|
135
|
+
spec = importlib.util.spec_from_file_location("episodic", path)
|
|
136
|
+
if spec and spec.loader:
|
|
137
|
+
episodic_module = importlib.util.module_from_spec(spec)
|
|
138
|
+
spec.loader.exec_module(episodic_module)
|
|
139
|
+
logger.debug(f"Loaded episodic module from {path}")
|
|
140
|
+
break
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.debug(f"Could not load episodic from {path}: {e}")
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
if not episodic_module:
|
|
146
|
+
logger.debug("Episodic memory module not found - skipping episode capture")
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
memory = episodic_module.EpisodicMemory()
|
|
150
|
+
|
|
151
|
+
# Use the real task description captured from the transcript.
|
|
152
|
+
# metrics["prompt"] now holds the first user message (task description)
|
|
153
|
+
# rather than the generic "SubagentStop for <agent>".
|
|
154
|
+
prompt = metrics.get("prompt", "")
|
|
155
|
+
if not prompt:
|
|
156
|
+
prompt = f"Task for {metrics.get('agent', 'unknown')}"
|
|
157
|
+
|
|
158
|
+
subagent_type = metrics.get("agent", "unknown")
|
|
159
|
+
duration_seconds = metrics.get("duration_ms", 0) / 1000.0 if metrics.get("duration_ms") else None
|
|
160
|
+
|
|
161
|
+
# Determine outcome: prefer plan_status string, fall back to exit_code
|
|
162
|
+
plan_status = metrics.get("plan_status", "")
|
|
163
|
+
exit_code = metrics.get("exit_code", 0)
|
|
164
|
+
if plan_status:
|
|
165
|
+
if "COMPLETE" in plan_status:
|
|
166
|
+
outcome = "success"
|
|
167
|
+
success = True
|
|
168
|
+
elif "BLOCKED" in plan_status or "ERROR" in plan_status:
|
|
169
|
+
outcome = "failed"
|
|
170
|
+
success = False
|
|
171
|
+
else:
|
|
172
|
+
# IN_PROGRESS, REVIEW, NEEDS_INPUT -> partial
|
|
173
|
+
outcome = "partial"
|
|
174
|
+
success = None
|
|
175
|
+
elif exit_code == 0:
|
|
176
|
+
outcome = "success"
|
|
177
|
+
success = True
|
|
178
|
+
else:
|
|
179
|
+
outcome = "failed"
|
|
180
|
+
success = False
|
|
181
|
+
|
|
182
|
+
# Tags from metrics -- filter empty strings defensively
|
|
183
|
+
tags = [t for t in metrics.get("tags", []) if t]
|
|
184
|
+
if not tags and subagent_type and subagent_type != "unknown":
|
|
185
|
+
tags = [subagent_type]
|
|
186
|
+
|
|
187
|
+
# Enrich with session events and anomalies
|
|
188
|
+
session_events = get_session_events()
|
|
189
|
+
context = {"metrics": metrics}
|
|
190
|
+
if session_events:
|
|
191
|
+
context["session_events"] = session_events
|
|
192
|
+
logger.info(f"Enriched episode with session events: {list(session_events.keys())}")
|
|
193
|
+
if anomalies:
|
|
194
|
+
context["anomalies"] = anomalies
|
|
195
|
+
logger.info(f"Episode has {len(anomalies)} anomaly/anomalies")
|
|
196
|
+
|
|
197
|
+
# Include context anchor hit tracking if available
|
|
198
|
+
anchor_hits = metrics.get("context_anchor_hits")
|
|
199
|
+
if anchor_hits:
|
|
200
|
+
context["context_anchor_hits"] = anchor_hits
|
|
201
|
+
logger.info(
|
|
202
|
+
"Episode anchor hits: %d/%d (%.0f%%)",
|
|
203
|
+
anchor_hits.get("hits", 0),
|
|
204
|
+
anchor_hits.get("total_checked", 0),
|
|
205
|
+
anchor_hits.get("hit_rate", 0) * 100,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# P3 CLI compatibility fields
|
|
209
|
+
episode_id = memory.store_episode(
|
|
210
|
+
prompt=prompt,
|
|
211
|
+
clarifications={},
|
|
212
|
+
enriched_prompt=prompt,
|
|
213
|
+
context=context,
|
|
214
|
+
tags=tags,
|
|
215
|
+
outcome=outcome,
|
|
216
|
+
success=success,
|
|
217
|
+
duration_seconds=duration_seconds,
|
|
218
|
+
commands_executed=commands_executed or [],
|
|
219
|
+
workflow_metrics=metrics,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
logger.info(f"Captured episode: {episode_id} (outcome: {outcome}, plan_status: {plan_status})")
|
|
223
|
+
return episode_id
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.debug(f"Failed to capture episodic memory: {e}")
|
|
227
|
+
return None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Orchestrator enforcement modules.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Orchestrator delegate mode enforcement.
|
|
2
|
+
|
|
3
|
+
When GAIA is installed, delegate mode is always active. The orchestrator
|
|
4
|
+
(main session) is restricted to dispatch-only tools. Direct investigation
|
|
5
|
+
tools (Bash, Read, Edit, etc.) are blocked so the orchestrator must
|
|
6
|
+
delegate to specialist agents.
|
|
7
|
+
|
|
8
|
+
Detection: Claude Code includes ``agent_id`` and ``agent_type`` in the
|
|
9
|
+
PreToolUse payload ONLY when the hook fires inside a subagent. Their absence
|
|
10
|
+
means the call originates from the main session (orchestrator).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Any, Dict, Optional
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Tools the orchestrator is allowed to use in delegate mode.
|
|
22
|
+
# Everything NOT in this set is blocked for the main session.
|
|
23
|
+
ORCHESTRATOR_ALLOWED_TOOLS = frozenset({
|
|
24
|
+
# Dispatch and communication
|
|
25
|
+
"agent",
|
|
26
|
+
"task",
|
|
27
|
+
"sendmessage",
|
|
28
|
+
|
|
29
|
+
# On-demand skills / procedures
|
|
30
|
+
"skill",
|
|
31
|
+
|
|
32
|
+
# Agent teams task management
|
|
33
|
+
"taskcreate",
|
|
34
|
+
"taskupdate",
|
|
35
|
+
"tasklist",
|
|
36
|
+
"taskget",
|
|
37
|
+
|
|
38
|
+
# Tool discovery
|
|
39
|
+
"toolsearch",
|
|
40
|
+
|
|
41
|
+
# Web research (read-only, T0)
|
|
42
|
+
"websearch",
|
|
43
|
+
"webfetch",
|
|
44
|
+
|
|
45
|
+
# User interaction (built-in, may not always trigger hooks)
|
|
46
|
+
"askuserquestion",
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class DelegateModeResult:
|
|
52
|
+
"""Result of delegate mode check."""
|
|
53
|
+
|
|
54
|
+
blocked: bool
|
|
55
|
+
reason: Optional[str] = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_orchestrator_context(hook_payload: Dict[str, Any]) -> bool:
|
|
59
|
+
"""Determine if the hook is firing in the main session (orchestrator).
|
|
60
|
+
|
|
61
|
+
Claude Code includes ``agent_id`` in the PreToolUse payload only when
|
|
62
|
+
the tool call originates from a subagent. Its absence means the call
|
|
63
|
+
is from the main session.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
hook_payload: The full stdin JSON dict from Claude Code.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if this is the orchestrator (main session), False if subagent.
|
|
70
|
+
"""
|
|
71
|
+
agent_id = hook_payload.get("agent_id")
|
|
72
|
+
# agent_id is absent or empty string for the main session
|
|
73
|
+
return not agent_id
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def check_delegate_mode(
|
|
77
|
+
tool_name: str, hook_payload: Dict[str, Any]
|
|
78
|
+
) -> DelegateModeResult:
|
|
79
|
+
"""Check whether a tool call should be blocked by delegate mode.
|
|
80
|
+
|
|
81
|
+
This is the single entry point. Call it early in the PreToolUse flow.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
tool_name: The tool being invoked (e.g., "Bash", "Read", "Edit").
|
|
85
|
+
hook_payload: The full stdin JSON dict from Claude Code.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
DelegateModeResult with blocked=True and a reason if the call
|
|
89
|
+
should be denied, or blocked=False if it should proceed.
|
|
90
|
+
"""
|
|
91
|
+
is_orchestrator = is_orchestrator_context(hook_payload)
|
|
92
|
+
if not is_orchestrator:
|
|
93
|
+
# Subagents have full tool access -- delegate mode does not apply
|
|
94
|
+
agent_id = hook_payload.get("agent_id", "<none>")
|
|
95
|
+
logger.debug(
|
|
96
|
+
"delegate_mode check: SKIP (subagent %s) tool=%s",
|
|
97
|
+
agent_id,
|
|
98
|
+
tool_name,
|
|
99
|
+
)
|
|
100
|
+
return DelegateModeResult(blocked=False)
|
|
101
|
+
|
|
102
|
+
normalized = tool_name.lower().strip()
|
|
103
|
+
if normalized in ORCHESTRATOR_ALLOWED_TOOLS:
|
|
104
|
+
logger.debug(
|
|
105
|
+
"delegate_mode check: ALLOW (orchestrator allowed tool) tool=%s",
|
|
106
|
+
tool_name,
|
|
107
|
+
)
|
|
108
|
+
return DelegateModeResult(blocked=False)
|
|
109
|
+
|
|
110
|
+
logger.warning(
|
|
111
|
+
"DELEGATE_MODE blocked tool '%s' for orchestrator (main session)",
|
|
112
|
+
tool_name,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return DelegateModeResult(
|
|
116
|
+
blocked=True,
|
|
117
|
+
reason=(
|
|
118
|
+
f"[DELEGATE MODE] Tool '{tool_name}' is not available to the orchestrator.\n\n"
|
|
119
|
+
f"As the orchestrator, you must delegate work to specialist agents.\n"
|
|
120
|
+
f"Use the Agent tool to dispatch to the appropriate agent, or use\n"
|
|
121
|
+
f"SendMessage to resume an existing agent.\n\n"
|
|
122
|
+
f"Allowed orchestrator tools: Agent, SendMessage, Skill, TaskCreate, "
|
|
123
|
+
f"TaskUpdate, TaskList, TaskGet, ToolSearch, WebSearch, WebFetch, "
|
|
124
|
+
f"AskUserQuestion.\n\n"
|
|
125
|
+
f"Do NOT attempt to use {tool_name} directly. Identify the right\n"
|
|
126
|
+
f"specialist agent and delegate the work."
|
|
127
|
+
),
|
|
128
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Lightweight scan trigger for SessionStart hook.
|
|
3
|
+
|
|
4
|
+
Runs a subset of project scanners (e.g., tools + environment) to refresh
|
|
5
|
+
project-context.json without significant startup delay (<3s target).
|
|
6
|
+
|
|
7
|
+
Uses the scan engine directly (in-process) — no dependency on bin/gaia-scan.py.
|
|
8
|
+
Works in both npm and plugin mode since tools/scan/ is always available.
|
|
9
|
+
|
|
10
|
+
Public API:
|
|
11
|
+
- trigger_lightweight_scan(project_root: Path, scanners: list) -> bool
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import List
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def trigger_lightweight_scan(
|
|
24
|
+
project_root: Path,
|
|
25
|
+
scanners: List[str] = None,
|
|
26
|
+
) -> bool:
|
|
27
|
+
"""Run a lightweight scan using the scan engine directly.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
project_root: Working directory for the scan.
|
|
31
|
+
scanners: List of scanner names to run. Defaults to
|
|
32
|
+
["tools", "environment"].
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
True on success, False on failure. Designed to complete in <3s.
|
|
36
|
+
"""
|
|
37
|
+
if scanners is None:
|
|
38
|
+
scanners = ["tools", "environment"]
|
|
39
|
+
|
|
40
|
+
# Ensure tools.scan is importable by adding plugin root to sys.path
|
|
41
|
+
hooks_dir = Path(__file__).resolve().parents[2] # hooks/
|
|
42
|
+
plugin_root = hooks_dir.parent
|
|
43
|
+
if str(plugin_root) not in sys.path:
|
|
44
|
+
sys.path.insert(0, str(plugin_root))
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
from tools.scan.config import ScanConfig
|
|
48
|
+
from tools.scan.orchestrator import ScanOrchestrator
|
|
49
|
+
from tools.scan.registry import ScannerRegistry
|
|
50
|
+
except ImportError as e:
|
|
51
|
+
logger.warning("Cannot import scan engine: %s", e)
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
start = time.monotonic()
|
|
56
|
+
|
|
57
|
+
config = ScanConfig(
|
|
58
|
+
scanners=scanners,
|
|
59
|
+
project_root=project_root,
|
|
60
|
+
)
|
|
61
|
+
registry = ScannerRegistry()
|
|
62
|
+
orchestrator = ScanOrchestrator(registry=registry, config=config)
|
|
63
|
+
output = orchestrator.run(project_root=project_root)
|
|
64
|
+
elapsed = time.monotonic() - start
|
|
65
|
+
|
|
66
|
+
if output.errors:
|
|
67
|
+
logger.warning(
|
|
68
|
+
"Lightweight scan completed with errors in %.1fs: %s",
|
|
69
|
+
elapsed,
|
|
70
|
+
output.errors[:3],
|
|
71
|
+
)
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
logger.info(
|
|
75
|
+
"Lightweight scan completed in %.1fs (scanners: %s, sections: %d)",
|
|
76
|
+
elapsed,
|
|
77
|
+
", ".join(scanners),
|
|
78
|
+
output.sections_updated,
|
|
79
|
+
)
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
except Exception as e:
|
|
83
|
+
logger.warning("Failed to run lightweight scan: %s", e)
|
|
84
|
+
return False
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security module - Security tiers, blocked patterns, mutative verb detection.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- tiers: SecurityTier enum and classification
|
|
6
|
+
- blocked_commands: Permanently blocked pattern matching
|
|
7
|
+
- mutative_verbs: Mutative verb detection (user approval workflow)
|
|
8
|
+
- gitops_validator: kubectl/helm/flux validation
|
|
9
|
+
- approval_constants: Approval token patterns (legacy APPROVE: and ElicitationResult)
|
|
10
|
+
- approval_grants: Time-limited T3 command passthrough after user approval
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .tiers import SecurityTier, classify_command_tier
|
|
14
|
+
from .command_semantics import analyze_command, CommandSemantics
|
|
15
|
+
from .blocked_commands import (
|
|
16
|
+
is_blocked_command,
|
|
17
|
+
get_blocked_patterns,
|
|
18
|
+
BlockedCommandResult,
|
|
19
|
+
)
|
|
20
|
+
from .gitops_validator import validate_gitops_workflow, GitOpsValidationResult
|
|
21
|
+
from .mutative_verbs import (
|
|
22
|
+
CLI_FAMILY_LOOKUP,
|
|
23
|
+
CATEGORY_MUTATIVE,
|
|
24
|
+
CATEGORY_SIMULATION,
|
|
25
|
+
CATEGORY_READ_ONLY,
|
|
26
|
+
CATEGORY_UNKNOWN,
|
|
27
|
+
)
|
|
28
|
+
from .approval_constants import NONCE_APPROVAL_PATTERN, NONCE_APPROVAL_PREFIX
|
|
29
|
+
from .approval_messages import (
|
|
30
|
+
CANONICAL_APPROVAL_TOKEN,
|
|
31
|
+
CANONICAL_APPROVAL_TOKEN_FORMAT,
|
|
32
|
+
CANONICAL_APPROVAL_TOKEN_GUIDANCE,
|
|
33
|
+
CANONICAL_APPROVAL_FORMAT_GUIDANCE,
|
|
34
|
+
LATEST_BLOCKED_COMMAND_PHRASE,
|
|
35
|
+
)
|
|
36
|
+
from .approval_scopes import (
|
|
37
|
+
ApprovalSignature,
|
|
38
|
+
SCOPE_EXACT_COMMAND,
|
|
39
|
+
SCOPE_SEMANTIC_SIGNATURE,
|
|
40
|
+
build_approval_signature,
|
|
41
|
+
matches_approval_signature,
|
|
42
|
+
)
|
|
43
|
+
from .approval_grants import (
|
|
44
|
+
check_approval_grant,
|
|
45
|
+
cleanup_expired_grants,
|
|
46
|
+
get_latest_pending_approval,
|
|
47
|
+
last_check_found_expired,
|
|
48
|
+
ApprovalGrant,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
# Tiers
|
|
53
|
+
"SecurityTier",
|
|
54
|
+
"classify_command_tier",
|
|
55
|
+
"analyze_command",
|
|
56
|
+
"CommandSemantics",
|
|
57
|
+
# Blocked commands
|
|
58
|
+
"is_blocked_command",
|
|
59
|
+
"get_blocked_patterns",
|
|
60
|
+
"BlockedCommandResult",
|
|
61
|
+
# GitOps
|
|
62
|
+
"validate_gitops_workflow",
|
|
63
|
+
"GitOpsValidationResult",
|
|
64
|
+
# Mutative verbs
|
|
65
|
+
"CLI_FAMILY_LOOKUP",
|
|
66
|
+
"CATEGORY_MUTATIVE",
|
|
67
|
+
"CATEGORY_SIMULATION",
|
|
68
|
+
"CATEGORY_READ_ONLY",
|
|
69
|
+
"CATEGORY_UNKNOWN",
|
|
70
|
+
# Approval
|
|
71
|
+
"NONCE_APPROVAL_PREFIX",
|
|
72
|
+
"NONCE_APPROVAL_PATTERN",
|
|
73
|
+
"CANONICAL_APPROVAL_TOKEN",
|
|
74
|
+
"CANONICAL_APPROVAL_TOKEN_FORMAT",
|
|
75
|
+
"CANONICAL_APPROVAL_TOKEN_GUIDANCE",
|
|
76
|
+
"CANONICAL_APPROVAL_FORMAT_GUIDANCE",
|
|
77
|
+
"LATEST_BLOCKED_COMMAND_PHRASE",
|
|
78
|
+
"ApprovalSignature",
|
|
79
|
+
"SCOPE_EXACT_COMMAND",
|
|
80
|
+
"SCOPE_SEMANTIC_SIGNATURE",
|
|
81
|
+
"build_approval_signature",
|
|
82
|
+
"matches_approval_signature",
|
|
83
|
+
# Approval Grants
|
|
84
|
+
"check_approval_grant",
|
|
85
|
+
"cleanup_expired_grants",
|
|
86
|
+
"get_latest_pending_approval",
|
|
87
|
+
"last_check_found_expired",
|
|
88
|
+
"ApprovalGrant",
|
|
89
|
+
]
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Approval file cleanup for the subagent stop hook.
|
|
3
|
+
|
|
4
|
+
Cleans up pending approval files after an agent completes, using the current
|
|
5
|
+
per-nonce file layout under .claude/cache/approvals/pending-{nonce}.json.
|
|
6
|
+
|
|
7
|
+
Provides:
|
|
8
|
+
- cleanup(): Delete pending approval files that match agent session
|
|
9
|
+
- consume_approval_file(): Backward-compatible alias for cleanup()
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from ..core.paths import find_claude_dir
|
|
18
|
+
from ..core.state import get_session_id
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_approvals_dir() -> Path:
|
|
24
|
+
"""Return the approvals cache directory."""
|
|
25
|
+
return find_claude_dir() / "cache" / "approvals"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cleanup(agent_type: str, session_id: Optional[str] = None) -> bool:
|
|
29
|
+
"""
|
|
30
|
+
Delete pending-{nonce}.json files for the current session after agent completion.
|
|
31
|
+
|
|
32
|
+
Scans .claude/cache/approvals/ for pending files scoped to the current
|
|
33
|
+
session and removes them, preventing stale pending approvals from
|
|
34
|
+
accumulating after the agent run finishes.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
agent_type: The agent type that just completed (for logging).
|
|
38
|
+
session_id: Session ID to scope cleanup (defaults to CLAUDE_SESSION_ID).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True if any pending approval files were consumed, False otherwise.
|
|
42
|
+
"""
|
|
43
|
+
if session_id is None:
|
|
44
|
+
session_id = get_session_id()
|
|
45
|
+
|
|
46
|
+
approvals_dir = _get_approvals_dir()
|
|
47
|
+
if not approvals_dir.exists():
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
consumed = False
|
|
51
|
+
try:
|
|
52
|
+
for pending_file in approvals_dir.glob("pending-*.json"):
|
|
53
|
+
# Skip the per-session index files
|
|
54
|
+
if pending_file.name.startswith("pending-index-"):
|
|
55
|
+
continue
|
|
56
|
+
try:
|
|
57
|
+
data = json.loads(pending_file.read_text())
|
|
58
|
+
if data.get("session_id") != session_id:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
pending_file.unlink(missing_ok=True)
|
|
62
|
+
logger.info(
|
|
63
|
+
"Consumed pending approval for agent '%s' "
|
|
64
|
+
"(nonce: %s, command: %s)",
|
|
65
|
+
agent_type,
|
|
66
|
+
data.get("nonce", "unknown"),
|
|
67
|
+
data.get("command", "unknown"),
|
|
68
|
+
)
|
|
69
|
+
consumed = True
|
|
70
|
+
|
|
71
|
+
except (json.JSONDecodeError, TypeError):
|
|
72
|
+
# Corrupt file -- remove it
|
|
73
|
+
pending_file.unlink(missing_ok=True)
|
|
74
|
+
consumed = True
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.debug(
|
|
77
|
+
"Failed to process pending file %s (non-fatal): %s",
|
|
78
|
+
pending_file.name, e,
|
|
79
|
+
)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.debug("Failed to scan approvals dir (non-fatal): %s", e)
|
|
82
|
+
|
|
83
|
+
return consumed
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# Backward-compatible alias
|
|
87
|
+
consume_approval_file = cleanup
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Approval token patterns and deprecated approval phrases for T3 operation resumes.
|
|
2
|
+
|
|
3
|
+
The APPROVE: prefix is a legacy path (SendMessage-based nonce relay). The primary
|
|
4
|
+
approval flow now uses ElicitationResult (AskUserQuestion -> user clicks Approve).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
NONCE_APPROVAL_PREFIX = "APPROVE:"
|
|
10
|
+
NONCE_APPROVAL_PATTERN = re.compile(r"\bAPPROVE:([a-f0-9]{32})\b")
|
|
11
|
+
|
|
12
|
+
# Deprecated approval phrases that agents should not use.
|
|
13
|
+
# Moved here from pre_tool_use.py so all approval-related constants live together.
|
|
14
|
+
DEPRECATED_APPROVAL_PHRASES = (
|
|
15
|
+
"user approved:",
|
|
16
|
+
"user approval received",
|
|
17
|
+
"approved by user",
|
|
18
|
+
"approval confirmed",
|
|
19
|
+
"approved. execute",
|
|
20
|
+
"approved, execute",
|
|
21
|
+
"proceed with execution",
|
|
22
|
+
"confirmed. proceed",
|
|
23
|
+
)
|