@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,496 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime validation for agent response contracts.
|
|
3
|
+
|
|
4
|
+
Validates the structured JSON contract block returned by agents
|
|
5
|
+
(``json:contract`` fenced blocks parsed by ``contract_validator.parse_contract``).
|
|
6
|
+
|
|
7
|
+
Validated sections:
|
|
8
|
+
- agent_status (plan_status, agent_id, pending_steps, next_action)
|
|
9
|
+
- evidence_report (patterns_checked, files_checked, commands_run, key_outputs, ...)
|
|
10
|
+
- consolidation_report (ownership_assessment, confirmed_findings, ...)
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import asdict, dataclass
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Dict, List, Optional
|
|
23
|
+
|
|
24
|
+
from ..core.paths import get_session_dir
|
|
25
|
+
from ..core.state import get_session_id
|
|
26
|
+
from .contract_validator import parse_contract
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
VALID_PLAN_STATUSES = {
|
|
30
|
+
"IN_PROGRESS",
|
|
31
|
+
"REVIEW",
|
|
32
|
+
"COMPLETE",
|
|
33
|
+
"BLOCKED",
|
|
34
|
+
"NEEDS_INPUT",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Evidence is required for ALL valid states -- no exclusions.
|
|
38
|
+
EVIDENCE_REQUIRED_PLAN_STATUSES = VALID_PLAN_STATUSES
|
|
39
|
+
|
|
40
|
+
EVIDENCE_FIELDS = [
|
|
41
|
+
"PATTERNS_CHECKED",
|
|
42
|
+
"FILES_CHECKED",
|
|
43
|
+
"COMMANDS_RUN",
|
|
44
|
+
"KEY_OUTPUTS",
|
|
45
|
+
"VERBATIM_OUTPUTS",
|
|
46
|
+
"CROSS_LAYER_IMPACTS",
|
|
47
|
+
"OPEN_GAPS",
|
|
48
|
+
]
|
|
49
|
+
VALID_OWNERSHIP_ASSESSMENTS = {
|
|
50
|
+
"owned_here",
|
|
51
|
+
"cross_surface_dependency",
|
|
52
|
+
"not_my_surface",
|
|
53
|
+
}
|
|
54
|
+
# Bullet-list fields only; OWNERSHIP_ASSESSMENT is validated separately as a key-value enum.
|
|
55
|
+
CONSOLIDATION_FIELDS = [
|
|
56
|
+
"CONFIRMED_FINDINGS",
|
|
57
|
+
"SUSPECTED_FINDINGS",
|
|
58
|
+
"CONFLICTS",
|
|
59
|
+
"OPEN_GAPS",
|
|
60
|
+
"NEXT_BEST_AGENT",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
RECOMMENDED_ACTION_NONE = "none"
|
|
64
|
+
|
|
65
|
+
# Statuses that should carry an approval_request block
|
|
66
|
+
APPROVAL_REQUEST_STATUSES = {"REVIEW"}
|
|
67
|
+
|
|
68
|
+
APPROVAL_REQUEST_REQUIRED_FIELDS = [
|
|
69
|
+
"operation",
|
|
70
|
+
"exact_content",
|
|
71
|
+
"scope",
|
|
72
|
+
"risk_level",
|
|
73
|
+
"rollback",
|
|
74
|
+
"verification",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
VALID_RISK_LEVELS = {"LOW", "MEDIUM", "HIGH", "CRITICAL"}
|
|
78
|
+
|
|
79
|
+
_NONCE_HEX_PATTERN = re.compile(r"^[a-f0-9]{32}$")
|
|
80
|
+
|
|
81
|
+
_AGENT_ID_PATTERN = re.compile(r"^a[0-9a-f]{5,}$")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass(frozen=True)
|
|
85
|
+
class AgentStatusBlock:
|
|
86
|
+
marker_present: bool
|
|
87
|
+
plan_status: str
|
|
88
|
+
pending_steps: str
|
|
89
|
+
next_action: str
|
|
90
|
+
agent_id: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass(frozen=True)
|
|
94
|
+
class EvidenceReportBlock:
|
|
95
|
+
marker_present: bool
|
|
96
|
+
fields: Dict[str, List[str]]
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class ConsolidationReportBlock:
|
|
101
|
+
marker_present: bool
|
|
102
|
+
ownership_assessment: str
|
|
103
|
+
fields: Dict[str, List[str]]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True)
|
|
107
|
+
class ResponseContractValidation:
|
|
108
|
+
valid: bool
|
|
109
|
+
severity: str
|
|
110
|
+
missing: List[str]
|
|
111
|
+
invalid: List[str]
|
|
112
|
+
warnings: List[str]
|
|
113
|
+
evidence_required: bool
|
|
114
|
+
consolidation_required: bool
|
|
115
|
+
recommended_action: str
|
|
116
|
+
agent_status: AgentStatusBlock
|
|
117
|
+
evidence_report: EvidenceReportBlock
|
|
118
|
+
consolidation_report: ConsolidationReportBlock
|
|
119
|
+
|
|
120
|
+
def to_dict(self) -> Dict[str, object]:
|
|
121
|
+
return asdict(self)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ============================================================================
|
|
125
|
+
# JSON contract -> dataclass extraction helpers
|
|
126
|
+
# ============================================================================
|
|
127
|
+
|
|
128
|
+
def _get_str(d: dict, key: str) -> str:
|
|
129
|
+
"""Get a string value from a dict, trying both lower-case and UPPER-CASE keys."""
|
|
130
|
+
return str(d.get(key, "") or d.get(key.upper(), "") or "")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _get_list(d: dict, key: str) -> List[str]:
|
|
134
|
+
"""Get a list-of-strings value, trying both lower-case and UPPER-CASE keys.
|
|
135
|
+
|
|
136
|
+
If the value is a list of dicts (e.g. commands_run entries with {command, result}),
|
|
137
|
+
each dict is serialised to a readable string.
|
|
138
|
+
"""
|
|
139
|
+
val = d.get(key) or d.get(key.upper()) or []
|
|
140
|
+
if not isinstance(val, list):
|
|
141
|
+
return [str(val)] if val else []
|
|
142
|
+
result: List[str] = []
|
|
143
|
+
for item in val:
|
|
144
|
+
if isinstance(item, dict):
|
|
145
|
+
# e.g. {"command": "ls", "result": "ok"} -> "`ls` -> ok"
|
|
146
|
+
cmd = item.get("command", item.get("cmd", ""))
|
|
147
|
+
res = item.get("result", item.get("output", ""))
|
|
148
|
+
result.append(f"`{cmd}` -> {res}" if cmd else str(item))
|
|
149
|
+
else:
|
|
150
|
+
result.append(str(item))
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _extract_agent_status(contract: dict) -> AgentStatusBlock:
|
|
155
|
+
"""Build an AgentStatusBlock from the parsed JSON contract dict."""
|
|
156
|
+
agent_status = contract.get("agent_status")
|
|
157
|
+
if not agent_status or not isinstance(agent_status, dict):
|
|
158
|
+
return AgentStatusBlock(
|
|
159
|
+
marker_present=False,
|
|
160
|
+
plan_status="",
|
|
161
|
+
pending_steps="",
|
|
162
|
+
next_action="",
|
|
163
|
+
agent_id="",
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
plan_status = _get_str(agent_status, "plan_status").upper().rstrip(".,;")
|
|
167
|
+
pending_steps = _get_str(agent_status, "pending_steps")
|
|
168
|
+
next_action = _get_str(agent_status, "next_action")
|
|
169
|
+
agent_id = _get_str(agent_status, "agent_id")
|
|
170
|
+
|
|
171
|
+
return AgentStatusBlock(
|
|
172
|
+
marker_present=True,
|
|
173
|
+
plan_status=plan_status,
|
|
174
|
+
pending_steps=pending_steps,
|
|
175
|
+
next_action=next_action,
|
|
176
|
+
agent_id=agent_id,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _extract_evidence_report(contract: dict) -> EvidenceReportBlock:
|
|
181
|
+
"""Build an EvidenceReportBlock from the parsed JSON contract dict."""
|
|
182
|
+
evidence = contract.get("evidence_report")
|
|
183
|
+
if not evidence or not isinstance(evidence, dict):
|
|
184
|
+
return EvidenceReportBlock(
|
|
185
|
+
marker_present=False,
|
|
186
|
+
fields={field: [] for field in EVIDENCE_FIELDS},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
fields: Dict[str, List[str]] = {}
|
|
190
|
+
for field_name in EVIDENCE_FIELDS:
|
|
191
|
+
key_lower = field_name.lower()
|
|
192
|
+
values = _get_list(evidence, key_lower)
|
|
193
|
+
fields[field_name] = values
|
|
194
|
+
|
|
195
|
+
return EvidenceReportBlock(marker_present=True, fields=fields)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _extract_consolidation_report(contract: dict) -> ConsolidationReportBlock:
|
|
199
|
+
"""Build a ConsolidationReportBlock from the parsed JSON contract dict."""
|
|
200
|
+
consolidation = contract.get("consolidation_report")
|
|
201
|
+
if not consolidation or not isinstance(consolidation, dict):
|
|
202
|
+
return ConsolidationReportBlock(
|
|
203
|
+
marker_present=False,
|
|
204
|
+
ownership_assessment="",
|
|
205
|
+
fields={field: [] for field in CONSOLIDATION_FIELDS},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
ownership = _get_str(consolidation, "ownership_assessment")
|
|
209
|
+
|
|
210
|
+
fields: Dict[str, List[str]] = {}
|
|
211
|
+
for field_name in CONSOLIDATION_FIELDS:
|
|
212
|
+
key_lower = field_name.lower()
|
|
213
|
+
values = _get_list(consolidation, key_lower)
|
|
214
|
+
fields[field_name] = values
|
|
215
|
+
|
|
216
|
+
return ConsolidationReportBlock(
|
|
217
|
+
marker_present=True,
|
|
218
|
+
ownership_assessment=ownership,
|
|
219
|
+
fields=fields,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# ============================================================================
|
|
224
|
+
# Public parse helpers (operate on agent_output string via parse_contract)
|
|
225
|
+
# ============================================================================
|
|
226
|
+
|
|
227
|
+
def parse_agent_status(agent_output: str, parsed_contract: Optional[dict] = None) -> AgentStatusBlock:
|
|
228
|
+
"""Parse agent_status from agent output using the json:contract block."""
|
|
229
|
+
contract = parsed_contract if parsed_contract is not None else parse_contract(agent_output)
|
|
230
|
+
if contract is None:
|
|
231
|
+
return AgentStatusBlock(
|
|
232
|
+
marker_present=False, plan_status="", pending_steps="",
|
|
233
|
+
next_action="", agent_id="",
|
|
234
|
+
)
|
|
235
|
+
return _extract_agent_status(contract)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def parse_evidence_report(agent_output: str, parsed_contract: Optional[dict] = None) -> EvidenceReportBlock:
|
|
239
|
+
"""Parse evidence_report from agent output using the json:contract block."""
|
|
240
|
+
contract = parsed_contract if parsed_contract is not None else parse_contract(agent_output)
|
|
241
|
+
if contract is None:
|
|
242
|
+
return EvidenceReportBlock(
|
|
243
|
+
marker_present=False,
|
|
244
|
+
fields={field: [] for field in EVIDENCE_FIELDS},
|
|
245
|
+
)
|
|
246
|
+
return _extract_evidence_report(contract)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def parse_consolidation_report(agent_output: str, parsed_contract: Optional[dict] = None) -> ConsolidationReportBlock:
|
|
250
|
+
"""Parse consolidation_report from agent output using the json:contract block."""
|
|
251
|
+
contract = parsed_contract if parsed_contract is not None else parse_contract(agent_output)
|
|
252
|
+
if contract is None:
|
|
253
|
+
return ConsolidationReportBlock(
|
|
254
|
+
marker_present=False, ownership_assessment="",
|
|
255
|
+
fields={field: [] for field in CONSOLIDATION_FIELDS},
|
|
256
|
+
)
|
|
257
|
+
return _extract_consolidation_report(contract)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _is_resume_agent_id(value: str) -> bool:
|
|
261
|
+
return bool(_AGENT_ID_PATTERN.match(value or ""))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def resolve_agent_id(task_info: dict) -> str:
|
|
265
|
+
"""Extract the agent ID from a task_info dict.
|
|
266
|
+
|
|
267
|
+
Falls back to ``task_id`` when ``agent_id`` is absent.
|
|
268
|
+
"""
|
|
269
|
+
return str(task_info.get("agent_id", "") or task_info.get("task_id", ""))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def validate_response_contract(
|
|
273
|
+
agent_output: str,
|
|
274
|
+
*,
|
|
275
|
+
task_agent_id: str = "",
|
|
276
|
+
consolidation_required: bool = False,
|
|
277
|
+
parsed_contract: Optional[dict] = None,
|
|
278
|
+
) -> ResponseContractValidation:
|
|
279
|
+
"""Validate deterministic response blocks emitted by an agent.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
agent_output: Raw agent output text.
|
|
283
|
+
task_agent_id: Agent ID from task_info, used as fallback.
|
|
284
|
+
consolidation_required: Whether a CONSOLIDATION_REPORT is required.
|
|
285
|
+
parsed_contract: Pre-parsed dict from parse_contract(). If provided,
|
|
286
|
+
avoids re-parsing agent_output. If None, parse_contract() is
|
|
287
|
+
called internally.
|
|
288
|
+
"""
|
|
289
|
+
contract = parsed_contract if parsed_contract is not None else parse_contract(agent_output)
|
|
290
|
+
|
|
291
|
+
if contract is None:
|
|
292
|
+
# No json:contract block found -- everything is missing.
|
|
293
|
+
empty_evidence = EvidenceReportBlock(
|
|
294
|
+
marker_present=False,
|
|
295
|
+
fields={field: [] for field in EVIDENCE_FIELDS},
|
|
296
|
+
)
|
|
297
|
+
empty_consolidation = ConsolidationReportBlock(
|
|
298
|
+
marker_present=False, ownership_assessment="",
|
|
299
|
+
fields={field: [] for field in CONSOLIDATION_FIELDS},
|
|
300
|
+
)
|
|
301
|
+
empty_status = AgentStatusBlock(
|
|
302
|
+
marker_present=False, plan_status="", pending_steps="",
|
|
303
|
+
next_action="", agent_id="",
|
|
304
|
+
)
|
|
305
|
+
missing = ["AGENT_STATUS", "PLAN_STATUS", "PENDING_STEPS", "NEXT_ACTION", "AGENT_ID"]
|
|
306
|
+
recommended_action = "escalate_contract_repair" if not task_agent_id else "resume_same_agent_contract_repair"
|
|
307
|
+
if not _is_resume_agent_id(task_agent_id):
|
|
308
|
+
recommended_action = "escalate_contract_repair"
|
|
309
|
+
return ResponseContractValidation(
|
|
310
|
+
valid=False,
|
|
311
|
+
severity="hard",
|
|
312
|
+
missing=missing,
|
|
313
|
+
invalid=[],
|
|
314
|
+
warnings=[],
|
|
315
|
+
evidence_required=False,
|
|
316
|
+
consolidation_required=consolidation_required,
|
|
317
|
+
recommended_action=recommended_action,
|
|
318
|
+
agent_status=empty_status,
|
|
319
|
+
evidence_report=empty_evidence,
|
|
320
|
+
consolidation_report=empty_consolidation,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
status = _extract_agent_status(contract)
|
|
324
|
+
evidence = _extract_evidence_report(contract)
|
|
325
|
+
if consolidation_required:
|
|
326
|
+
consolidation = _extract_consolidation_report(contract)
|
|
327
|
+
else:
|
|
328
|
+
consolidation = ConsolidationReportBlock(
|
|
329
|
+
marker_present=False, ownership_assessment="",
|
|
330
|
+
fields={field: [] for field in CONSOLIDATION_FIELDS}
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
missing: List[str] = []
|
|
334
|
+
invalid: List[str] = []
|
|
335
|
+
|
|
336
|
+
if not status.marker_present:
|
|
337
|
+
missing.append("AGENT_STATUS")
|
|
338
|
+
|
|
339
|
+
if not status.plan_status:
|
|
340
|
+
missing.append("PLAN_STATUS")
|
|
341
|
+
elif status.plan_status not in VALID_PLAN_STATUSES:
|
|
342
|
+
invalid.append(f"PLAN_STATUS:{status.plan_status}")
|
|
343
|
+
|
|
344
|
+
if not status.pending_steps:
|
|
345
|
+
missing.append("PENDING_STEPS")
|
|
346
|
+
if not status.next_action:
|
|
347
|
+
missing.append("NEXT_ACTION")
|
|
348
|
+
if not status.agent_id:
|
|
349
|
+
missing.append("AGENT_ID")
|
|
350
|
+
|
|
351
|
+
effective_agent_id = status.agent_id if _is_resume_agent_id(status.agent_id) else task_agent_id
|
|
352
|
+
if not _is_resume_agent_id(effective_agent_id):
|
|
353
|
+
effective_agent_id = ""
|
|
354
|
+
evidence_required = status.plan_status in EVIDENCE_REQUIRED_PLAN_STATUSES
|
|
355
|
+
if evidence_required:
|
|
356
|
+
if not evidence.marker_present:
|
|
357
|
+
missing.append("EVIDENCE_REPORT")
|
|
358
|
+
for field in EVIDENCE_FIELDS:
|
|
359
|
+
if not evidence.fields.get(field, []):
|
|
360
|
+
missing.append(field)
|
|
361
|
+
|
|
362
|
+
if consolidation_required:
|
|
363
|
+
if not consolidation.marker_present:
|
|
364
|
+
missing.append("CONSOLIDATION_REPORT")
|
|
365
|
+
if not consolidation.ownership_assessment:
|
|
366
|
+
missing.append("OWNERSHIP_ASSESSMENT")
|
|
367
|
+
elif consolidation.ownership_assessment not in VALID_OWNERSHIP_ASSESSMENTS:
|
|
368
|
+
invalid.append(f"OWNERSHIP_ASSESSMENT:{consolidation.ownership_assessment}")
|
|
369
|
+
for field in CONSOLIDATION_FIELDS:
|
|
370
|
+
if not consolidation.fields.get(field, []):
|
|
371
|
+
missing.append(field)
|
|
372
|
+
|
|
373
|
+
# ------------------------------------------------------------------
|
|
374
|
+
# Approval request validation (advisory -- warnings only, not blocking)
|
|
375
|
+
# ------------------------------------------------------------------
|
|
376
|
+
warnings: List[str] = []
|
|
377
|
+
if status.plan_status in APPROVAL_REQUEST_STATUSES:
|
|
378
|
+
approval_req = contract.get("approval_request")
|
|
379
|
+
if not approval_req or not isinstance(approval_req, dict):
|
|
380
|
+
warnings.append("APPROVAL_REQUEST_MISSING")
|
|
381
|
+
else:
|
|
382
|
+
for field in APPROVAL_REQUEST_REQUIRED_FIELDS:
|
|
383
|
+
if not approval_req.get(field):
|
|
384
|
+
warnings.append(f"APPROVAL_REQUEST_FIELD_MISSING:{field}")
|
|
385
|
+
risk = str(approval_req.get("risk_level", "")).upper()
|
|
386
|
+
if risk and risk not in VALID_RISK_LEVELS:
|
|
387
|
+
warnings.append(f"APPROVAL_REQUEST_INVALID_RISK_LEVEL:{risk}")
|
|
388
|
+
# Check for approval_id when status is REVIEW
|
|
389
|
+
if status.plan_status == "REVIEW":
|
|
390
|
+
pass # approval_id presence is advisory, not enforced
|
|
391
|
+
|
|
392
|
+
valid = not missing and not invalid
|
|
393
|
+
recommended_action = RECOMMENDED_ACTION_NONE if valid else "resume_same_agent_contract_repair"
|
|
394
|
+
severity = "none" if valid else "hard"
|
|
395
|
+
|
|
396
|
+
# If there is no actionable agent id, repair cannot be routed deterministically.
|
|
397
|
+
if not valid and not effective_agent_id:
|
|
398
|
+
recommended_action = "escalate_contract_repair"
|
|
399
|
+
|
|
400
|
+
return ResponseContractValidation(
|
|
401
|
+
valid=valid,
|
|
402
|
+
severity=severity,
|
|
403
|
+
missing=missing,
|
|
404
|
+
invalid=invalid,
|
|
405
|
+
warnings=warnings,
|
|
406
|
+
evidence_required=evidence_required,
|
|
407
|
+
consolidation_required=consolidation_required,
|
|
408
|
+
recommended_action=recommended_action,
|
|
409
|
+
agent_status=status,
|
|
410
|
+
evidence_report=evidence,
|
|
411
|
+
consolidation_report=consolidation,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _get_session_id() -> str:
|
|
416
|
+
return get_session_id()
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
_contract_dir_cache: Dict[str, Path] = {}
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def clear_contract_dir_cache() -> None:
|
|
423
|
+
"""Clear the cached contract directory path (useful for testing)."""
|
|
424
|
+
_contract_dir_cache.clear()
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _get_contract_dir(session_id: Optional[str] = None) -> Path:
|
|
428
|
+
session_id = session_id or _get_session_id()
|
|
429
|
+
cached = _contract_dir_cache.get(session_id)
|
|
430
|
+
if cached is not None and cached.is_dir():
|
|
431
|
+
return cached
|
|
432
|
+
path = get_session_dir() / "response-contract" / session_id
|
|
433
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
434
|
+
_contract_dir_cache[session_id] = path
|
|
435
|
+
return path
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _read_json(path: Path) -> Optional[dict]:
|
|
439
|
+
try:
|
|
440
|
+
return json.loads(path.read_text())
|
|
441
|
+
except (FileNotFoundError, json.JSONDecodeError, OSError):
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def load_last_validation(session_id: Optional[str] = None) -> Optional[dict]:
|
|
446
|
+
"""Load the last response-contract validation result, if any."""
|
|
447
|
+
session_id = session_id or _get_session_id()
|
|
448
|
+
path = _get_contract_dir(session_id) / "last-result.json"
|
|
449
|
+
payload = _read_json(path)
|
|
450
|
+
if not payload:
|
|
451
|
+
return None
|
|
452
|
+
if payload.get("session_id") != session_id:
|
|
453
|
+
return None
|
|
454
|
+
return payload
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def save_validation_result(task_info: Dict[str, object], validation: ResponseContractValidation) -> Path:
|
|
458
|
+
"""Persist the last validation result for observability and orchestration."""
|
|
459
|
+
session_id = _get_session_id()
|
|
460
|
+
target = _get_contract_dir(session_id) / "last-result.json"
|
|
461
|
+
payload = {
|
|
462
|
+
"timestamp": datetime.now().isoformat(),
|
|
463
|
+
"created_at_epoch": time.time(),
|
|
464
|
+
"session_id": session_id,
|
|
465
|
+
"agent": task_info.get("agent", ""),
|
|
466
|
+
"agent_id": resolve_agent_id(task_info),
|
|
467
|
+
"task_id": task_info.get("task_id", ""),
|
|
468
|
+
"validation": validation.to_dict(),
|
|
469
|
+
}
|
|
470
|
+
target.write_text(json.dumps(payload, indent=2))
|
|
471
|
+
return target
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
__all__ = [
|
|
475
|
+
"AgentStatusBlock",
|
|
476
|
+
"EvidenceReportBlock",
|
|
477
|
+
"ConsolidationReportBlock",
|
|
478
|
+
"ResponseContractValidation",
|
|
479
|
+
"VALID_PLAN_STATUSES",
|
|
480
|
+
"EVIDENCE_REQUIRED_PLAN_STATUSES",
|
|
481
|
+
"EVIDENCE_FIELDS",
|
|
482
|
+
"VALID_OWNERSHIP_ASSESSMENTS",
|
|
483
|
+
"CONSOLIDATION_FIELDS",
|
|
484
|
+
"RECOMMENDED_ACTION_NONE",
|
|
485
|
+
"APPROVAL_REQUEST_STATUSES",
|
|
486
|
+
"APPROVAL_REQUEST_REQUIRED_FIELDS",
|
|
487
|
+
"VALID_RISK_LEVELS",
|
|
488
|
+
"parse_agent_status",
|
|
489
|
+
"parse_evidence_report",
|
|
490
|
+
"parse_consolidation_report",
|
|
491
|
+
"validate_response_contract",
|
|
492
|
+
"save_validation_result",
|
|
493
|
+
"load_last_validation",
|
|
494
|
+
"resolve_agent_id",
|
|
495
|
+
"clear_contract_dir_cache",
|
|
496
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Skill injection verifier -- transcript fingerprint checking.
|
|
3
|
+
|
|
4
|
+
At SubagentStop, verifies that skills declared in the agent's frontmatter
|
|
5
|
+
were actually injected into the agent's context by searching for unique
|
|
6
|
+
fingerprint strings from each SKILL.md.
|
|
7
|
+
|
|
8
|
+
Returns an optional anomaly dict (advisory) when declared skills are missing
|
|
9
|
+
from the transcript, indicating a potential injection gap.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Fingerprint strings per skill: unique phrases from SKILL.md that confirm injection.
|
|
19
|
+
# Each skill maps to a list of candidate fingerprints -- at least one must appear
|
|
20
|
+
# in the transcript for the skill to be considered present.
|
|
21
|
+
SKILL_FINGERPRINTS: Dict[str, List[str]] = {
|
|
22
|
+
"agent-protocol": [
|
|
23
|
+
"json:contract",
|
|
24
|
+
"plan_status",
|
|
25
|
+
"evidence_report",
|
|
26
|
+
],
|
|
27
|
+
"security-tiers": [
|
|
28
|
+
"T0_READ_ONLY",
|
|
29
|
+
"T3_BLOCKED",
|
|
30
|
+
"Tier Definitions",
|
|
31
|
+
"Hook Enforcement",
|
|
32
|
+
],
|
|
33
|
+
"investigation": [
|
|
34
|
+
"Start From Injected Context",
|
|
35
|
+
"Pattern Hierarchy",
|
|
36
|
+
"Codebase first",
|
|
37
|
+
],
|
|
38
|
+
"command-execution": [
|
|
39
|
+
"ONE COMMAND. ONE RESULT. ONE EXIT CODE",
|
|
40
|
+
"NO PIPES. NO CHAINS. NO REDIRECTS",
|
|
41
|
+
"cloud_pipe_validator",
|
|
42
|
+
],
|
|
43
|
+
"context-updater": [
|
|
44
|
+
"CONTEXT_UPDATE",
|
|
45
|
+
"context-updater",
|
|
46
|
+
],
|
|
47
|
+
"fast-queries": [
|
|
48
|
+
"fast-queries",
|
|
49
|
+
"triage",
|
|
50
|
+
],
|
|
51
|
+
"terraform-patterns": [
|
|
52
|
+
"terraform-patterns",
|
|
53
|
+
"Terragrunt",
|
|
54
|
+
],
|
|
55
|
+
"gitops-patterns": [
|
|
56
|
+
"gitops-patterns",
|
|
57
|
+
"Flux",
|
|
58
|
+
"HelmRelease",
|
|
59
|
+
],
|
|
60
|
+
"developer-patterns": [
|
|
61
|
+
"developer-patterns",
|
|
62
|
+
],
|
|
63
|
+
"speckit-workflow": [
|
|
64
|
+
"speckit-workflow",
|
|
65
|
+
"speckit",
|
|
66
|
+
],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def verify_skill_injection(
|
|
71
|
+
agent_type: str,
|
|
72
|
+
transcript_text: str,
|
|
73
|
+
declared_skills: List[str],
|
|
74
|
+
) -> Optional[Dict[str, Any]]:
|
|
75
|
+
"""Verify that declared skills were injected into the agent transcript.
|
|
76
|
+
|
|
77
|
+
Searches the transcript for fingerprint strings that confirm each skill
|
|
78
|
+
was loaded. Returns an anomaly dict if any declared skill has no
|
|
79
|
+
fingerprint match in the transcript.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
agent_type: The agent type string (e.g. "cloud-troubleshooter").
|
|
83
|
+
transcript_text: The full agent transcript text.
|
|
84
|
+
declared_skills: List of skill names from agent frontmatter.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
An anomaly dict (type: skill_injection_gap, severity: advisory) if
|
|
88
|
+
any declared skill is missing from the transcript. None if all
|
|
89
|
+
declared skills are present or if the check does not apply.
|
|
90
|
+
"""
|
|
91
|
+
if not transcript_text or not declared_skills:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
missing_skills: List[str] = []
|
|
95
|
+
|
|
96
|
+
for skill_name in declared_skills:
|
|
97
|
+
fingerprints = SKILL_FINGERPRINTS.get(skill_name)
|
|
98
|
+
if fingerprints is None:
|
|
99
|
+
# No fingerprints defined for this skill -- skip (cannot verify)
|
|
100
|
+
logger.debug(
|
|
101
|
+
"No fingerprints defined for skill '%s', skipping verification",
|
|
102
|
+
skill_name,
|
|
103
|
+
)
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
# At least one fingerprint must appear in the transcript
|
|
107
|
+
found = any(fp in transcript_text for fp in fingerprints)
|
|
108
|
+
if not found:
|
|
109
|
+
missing_skills.append(skill_name)
|
|
110
|
+
|
|
111
|
+
if not missing_skills:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
"type": "skill_injection_gap",
|
|
116
|
+
"severity": "advisory",
|
|
117
|
+
"agent_type": agent_type,
|
|
118
|
+
"missing_skills": missing_skills,
|
|
119
|
+
"message": (
|
|
120
|
+
f"Agent '{agent_type}' declared {len(declared_skills)} skills but "
|
|
121
|
+
f"{len(missing_skills)} skill(s) have no transcript fingerprint: "
|
|
122
|
+
f"{', '.join(missing_skills)}"
|
|
123
|
+
),
|
|
124
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Build task_info dict from Claude Code SubagentStop stdin payload.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- build_task_info_from_hook_data(): Map hook payload to task_info format
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any, Dict
|
|
11
|
+
|
|
12
|
+
from .contract_validator import extract_exit_code_from_output, extract_plan_status_from_output
|
|
13
|
+
from .transcript_reader import (
|
|
14
|
+
extract_injected_context_payload_from_transcript,
|
|
15
|
+
extract_task_description_from_transcript,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_task_info_from_hook_data(
|
|
22
|
+
hook_data: Dict[str, Any],
|
|
23
|
+
agent_output: str = "",
|
|
24
|
+
) -> Dict[str, Any]:
|
|
25
|
+
"""Build a task_info dict from the Claude Code SubagentStop stdin payload.
|
|
26
|
+
|
|
27
|
+
Claude Code sends these fields for SubagentStop:
|
|
28
|
+
- hook_event_name: "SubagentStop"
|
|
29
|
+
- session_id: str
|
|
30
|
+
- agent_type: str (e.g. "cloud-troubleshooter")
|
|
31
|
+
- agent_id: str
|
|
32
|
+
- transcript_path: str (session-level JSONL)
|
|
33
|
+
- agent_transcript_path: str (subagent JSONL)
|
|
34
|
+
- last_assistant_message: str (final agent response text, no I/O needed)
|
|
35
|
+
- cwd: str
|
|
36
|
+
- stop_hook_active: bool
|
|
37
|
+
- permission_mode: str
|
|
38
|
+
|
|
39
|
+
We map these to the task_info format expected by subagent_stop_hook().
|
|
40
|
+
The exit_code is derived from the agent's AGENT_STATUS block.
|
|
41
|
+
task_description is extracted from the first user message in the transcript.
|
|
42
|
+
tier_real is parsed from the AGENT_STATUS block (not hardcoded T0).
|
|
43
|
+
"""
|
|
44
|
+
exit_code = extract_exit_code_from_output(agent_output) if agent_output else 0
|
|
45
|
+
plan_status = extract_plan_status_from_output(agent_output) if agent_output else ""
|
|
46
|
+
|
|
47
|
+
# Extract tier from agent output (e.g. agents report tier in their context)
|
|
48
|
+
# Look for explicit tier references in agent output: T0, T1, T2, T3
|
|
49
|
+
tier_real = "T0"
|
|
50
|
+
if agent_output:
|
|
51
|
+
tier_match = re.search(r"\bT([0-3])\b", agent_output)
|
|
52
|
+
if tier_match:
|
|
53
|
+
tier_real = f"T{tier_match.group(1)}"
|
|
54
|
+
|
|
55
|
+
# Extract real task description from the first user message in the transcript
|
|
56
|
+
transcript_path = hook_data.get("agent_transcript_path", "")
|
|
57
|
+
task_description = extract_task_description_from_transcript(transcript_path)
|
|
58
|
+
injected_context = extract_injected_context_payload_from_transcript(transcript_path)
|
|
59
|
+
agent_type = hook_data.get("agent_type", "") or "unknown"
|
|
60
|
+
if not task_description:
|
|
61
|
+
task_description = f"SubagentStop for {agent_type}"
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
"task_id": hook_data.get("agent_id", "unknown"),
|
|
65
|
+
"agent_id": hook_data.get("agent_id", "unknown"),
|
|
66
|
+
"agent_transcript_path": transcript_path,
|
|
67
|
+
"description": task_description,
|
|
68
|
+
"agent": agent_type,
|
|
69
|
+
"tier": tier_real,
|
|
70
|
+
"tags": [agent_type] if agent_type != "unknown" else [],
|
|
71
|
+
"exit_code": exit_code,
|
|
72
|
+
"plan_status": plan_status,
|
|
73
|
+
"injected_context": injected_context,
|
|
74
|
+
}
|