@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,647 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contract validation for agent output: structural checks, evidence parsing,
|
|
3
|
+
command extraction, PLAN_STATUS parsing, and exit code derivation.
|
|
4
|
+
|
|
5
|
+
Only the ``json:contract`` fenced-block format is supported. Legacy
|
|
6
|
+
HTML-comment blocks (``<!-- AGENT_STATUS -->``, etc.) are **not** parsed here.
|
|
7
|
+
|
|
8
|
+
Provides:
|
|
9
|
+
- parse_contract(): Extract structured dict from json:contract fenced block
|
|
10
|
+
- validate(): Check agent output against contract requirements -> ValidationResult
|
|
11
|
+
- extract_commands_from_evidence(): Parse COMMANDS_RUN field
|
|
12
|
+
- requires_consolidation_report(): Check if consolidation is needed
|
|
13
|
+
- extract_plan_status_from_output(): Extract PLAN_STATUS string
|
|
14
|
+
- extract_exit_code_from_output(): Derive exit code from PLAN_STATUS
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
_NOT_RUN_INDICATORS = re.compile(
|
|
26
|
+
r"\b(not\s+run|not\s+executed|skipped|n/a)\b",
|
|
27
|
+
re.IGNORECASE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_LITERAL_NONE_COMMANDS = {"none", "not run", "not executed", "n/a", "skipped"}
|
|
31
|
+
|
|
32
|
+
# Required evidence fields
|
|
33
|
+
_EVIDENCE_REQUIRED_FIELDS = [
|
|
34
|
+
"PATTERNS_CHECKED", "FILES_CHECKED", "COMMANDS_RUN", "KEY_OUTPUTS",
|
|
35
|
+
"VERBATIM_OUTPUTS", "CROSS_LAYER_IMPACTS", "OPEN_GAPS",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Required consolidation fields
|
|
39
|
+
_CONSOLIDATION_REQUIRED_FIELDS = [
|
|
40
|
+
"OWNERSHIP_ASSESSMENT", "CONFIRMED_FINDINGS", "SUSPECTED_FINDINGS",
|
|
41
|
+
"CONFLICTS", "OPEN_GAPS", "NEXT_BEST_AGENT",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class ValidationResult:
|
|
47
|
+
"""Result of contract validation.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
is_valid: True if all required contract blocks are present and complete.
|
|
51
|
+
missing: List of missing block/field names.
|
|
52
|
+
error_message: Descriptive error for stderr output when is_valid is False.
|
|
53
|
+
"""
|
|
54
|
+
is_valid: bool
|
|
55
|
+
missing: List[str]
|
|
56
|
+
error_message: str
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ============================================================================
|
|
60
|
+
# JSON contract parser
|
|
61
|
+
# ============================================================================
|
|
62
|
+
|
|
63
|
+
def parse_contract(agent_output: str) -> Optional[dict]:
|
|
64
|
+
"""Extract structured contract dict from a ``json:contract`` fenced block.
|
|
65
|
+
|
|
66
|
+
Searches for the first occurrence of a fenced code block tagged
|
|
67
|
+
``json:contract`` and attempts to parse its contents as JSON.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
agent_output: Complete output from agent execution.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Parsed dict if a valid json:contract block is found, None otherwise.
|
|
74
|
+
"""
|
|
75
|
+
m = re.search(r'```json:contract\s*\n(.*?)```', agent_output, re.DOTALL)
|
|
76
|
+
if not m:
|
|
77
|
+
return None
|
|
78
|
+
try:
|
|
79
|
+
return json.loads(m.group(1))
|
|
80
|
+
except json.JSONDecodeError:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ============================================================================
|
|
85
|
+
# JSON contract validation helpers
|
|
86
|
+
# ============================================================================
|
|
87
|
+
|
|
88
|
+
def _validate_from_json_contract(contract: dict, task_info: Dict[str, Any]) -> ValidationResult:
|
|
89
|
+
"""Validate agent output using the parsed JSON contract dict.
|
|
90
|
+
|
|
91
|
+
Checks that the contract dict contains the required keys:
|
|
92
|
+
- agent_status with plan_status and agent_id
|
|
93
|
+
- evidence_report with required fields (when plan_status requires it)
|
|
94
|
+
- consolidation_report (when multi-surface task requires it)
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
contract: Parsed dict from parse_contract().
|
|
98
|
+
task_info: Task metadata including injected_context for multi-surface detection.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
ValidationResult with is_valid, missing fields list, and error_message.
|
|
102
|
+
"""
|
|
103
|
+
all_missing: List[str] = []
|
|
104
|
+
|
|
105
|
+
# 1. Check agent_status
|
|
106
|
+
agent_status = contract.get("agent_status")
|
|
107
|
+
if not agent_status or not isinstance(agent_status, dict):
|
|
108
|
+
all_missing.extend(["AGENT_STATUS", "PLAN_STATUS", "AGENT_ID"])
|
|
109
|
+
else:
|
|
110
|
+
if not agent_status.get("plan_status"):
|
|
111
|
+
all_missing.append("PLAN_STATUS")
|
|
112
|
+
if not agent_status.get("agent_id"):
|
|
113
|
+
all_missing.append("AGENT_ID")
|
|
114
|
+
|
|
115
|
+
# Determine plan_status for evidence check
|
|
116
|
+
plan_status = ""
|
|
117
|
+
if agent_status and isinstance(agent_status, dict):
|
|
118
|
+
plan_status = str(agent_status.get("plan_status", "")).upper()
|
|
119
|
+
|
|
120
|
+
statuses_requiring_evidence = {
|
|
121
|
+
"IN_PROGRESS", "REVIEW",
|
|
122
|
+
"COMPLETE", "BLOCKED", "NEEDS_INPUT",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if plan_status in statuses_requiring_evidence:
|
|
126
|
+
# 2. Check evidence_report
|
|
127
|
+
evidence = contract.get("evidence_report")
|
|
128
|
+
if not evidence or not isinstance(evidence, dict):
|
|
129
|
+
all_missing.append("EVIDENCE_REPORT")
|
|
130
|
+
else:
|
|
131
|
+
for field in _EVIDENCE_REQUIRED_FIELDS:
|
|
132
|
+
# Accept both lower-case keys (JSON style) and upper-case (legacy)
|
|
133
|
+
key_lower = field.lower()
|
|
134
|
+
if not evidence.get(key_lower) and not evidence.get(field):
|
|
135
|
+
all_missing.append(field)
|
|
136
|
+
|
|
137
|
+
# 3. Check consolidation_report (only when required)
|
|
138
|
+
if requires_consolidation_report(task_info):
|
|
139
|
+
consolidation = contract.get("consolidation_report")
|
|
140
|
+
if not consolidation or not isinstance(consolidation, dict):
|
|
141
|
+
all_missing.append("CONSOLIDATION_REPORT")
|
|
142
|
+
else:
|
|
143
|
+
for field in _CONSOLIDATION_REQUIRED_FIELDS:
|
|
144
|
+
key_lower = field.lower()
|
|
145
|
+
if not consolidation.get(key_lower) and not consolidation.get(field):
|
|
146
|
+
all_missing.append(field)
|
|
147
|
+
|
|
148
|
+
if all_missing:
|
|
149
|
+
fields_str = ", ".join(all_missing)
|
|
150
|
+
error_message = (
|
|
151
|
+
f"Contract incomplete. Missing: {fields_str}.\n"
|
|
152
|
+
f"\n"
|
|
153
|
+
f"Repair: reissue your response ending with a json:contract block:\n"
|
|
154
|
+
f"\n"
|
|
155
|
+
f"```json:contract\n"
|
|
156
|
+
f'{{\n'
|
|
157
|
+
f' "agent_status": {{\n'
|
|
158
|
+
f' "plan_status": "<STATUS>",\n'
|
|
159
|
+
f' "agent_id": "<your-id>",\n'
|
|
160
|
+
f' "pending_steps": [],\n'
|
|
161
|
+
f' "next_action": "<done or next step>"\n'
|
|
162
|
+
f" }},\n"
|
|
163
|
+
f' "evidence_report": {{\n'
|
|
164
|
+
f' "patterns_checked": [],\n'
|
|
165
|
+
f' "files_checked": [],\n'
|
|
166
|
+
f' "commands_run": [],\n'
|
|
167
|
+
f' "key_outputs": [],\n'
|
|
168
|
+
f' "verbatim_outputs": [],\n'
|
|
169
|
+
f' "cross_layer_impacts": [],\n'
|
|
170
|
+
f' "open_gaps": []\n'
|
|
171
|
+
f" }},\n"
|
|
172
|
+
f' "consolidation_report": null\n'
|
|
173
|
+
f"}}\n"
|
|
174
|
+
f"```\n"
|
|
175
|
+
f"\n"
|
|
176
|
+
f"Required fields: agent_status (plan_status, agent_id, pending_steps, next_action), evidence_report\n"
|
|
177
|
+
f"Evidence required fields: patterns_checked, files_checked, commands_run, key_outputs, verbatim_outputs, cross_layer_impacts, open_gaps"
|
|
178
|
+
)
|
|
179
|
+
return ValidationResult(
|
|
180
|
+
is_valid=False,
|
|
181
|
+
missing=all_missing,
|
|
182
|
+
error_message=error_message,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return ValidationResult(is_valid=True, missing=[], error_message="")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# ============================================================================
|
|
189
|
+
# Main validation entry point
|
|
190
|
+
# ============================================================================
|
|
191
|
+
|
|
192
|
+
def validate(agent_output: str, task_info: Dict[str, Any]) -> ValidationResult:
|
|
193
|
+
"""Validate agent output against contract requirements.
|
|
194
|
+
|
|
195
|
+
Only the ``json:contract`` fenced-block format is supported.
|
|
196
|
+
|
|
197
|
+
Checks:
|
|
198
|
+
1. AGENT_STATUS block with plan_status and agent_id
|
|
199
|
+
2. EVIDENCE_REPORT with required fields (when plan_status requires it)
|
|
200
|
+
3. CONSOLIDATION_REPORT (when multi-surface task requires it)
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
agent_output: Complete output from agent execution.
|
|
204
|
+
task_info: Task metadata including injected_context for multi-surface detection.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
ValidationResult with is_valid, missing fields list, and error_message.
|
|
208
|
+
"""
|
|
209
|
+
contract = parse_contract(agent_output)
|
|
210
|
+
if contract is not None:
|
|
211
|
+
return _validate_from_json_contract(contract, task_info)
|
|
212
|
+
|
|
213
|
+
# No json:contract block found -- report everything as missing.
|
|
214
|
+
all_missing = ["AGENT_STATUS", "PLAN_STATUS", "AGENT_ID"]
|
|
215
|
+
fields_str = ", ".join(all_missing)
|
|
216
|
+
error_message = (
|
|
217
|
+
f"Contract incomplete. Missing: {fields_str}. "
|
|
218
|
+
f"No json:contract fenced block found.\n"
|
|
219
|
+
f"\n"
|
|
220
|
+
f"Repair: your response MUST end with a json:contract block:\n"
|
|
221
|
+
f"\n"
|
|
222
|
+
f"```json:contract\n"
|
|
223
|
+
f'{{\n'
|
|
224
|
+
f' "agent_status": {{\n'
|
|
225
|
+
f' "plan_status": "<STATUS>",\n'
|
|
226
|
+
f' "agent_id": "<your-id>",\n'
|
|
227
|
+
f' "pending_steps": [],\n'
|
|
228
|
+
f' "next_action": "<done or next step>"\n'
|
|
229
|
+
f" }},\n"
|
|
230
|
+
f' "evidence_report": {{\n'
|
|
231
|
+
f' "patterns_checked": [],\n'
|
|
232
|
+
f' "files_checked": [],\n'
|
|
233
|
+
f' "commands_run": [],\n'
|
|
234
|
+
f' "key_outputs": [],\n'
|
|
235
|
+
f' "verbatim_outputs": [],\n'
|
|
236
|
+
f' "cross_layer_impacts": [],\n'
|
|
237
|
+
f' "open_gaps": []\n'
|
|
238
|
+
f" }},\n"
|
|
239
|
+
f' "consolidation_report": null\n'
|
|
240
|
+
f"}}\n"
|
|
241
|
+
f"```\n"
|
|
242
|
+
f"\n"
|
|
243
|
+
f"Required fields: agent_status (plan_status, agent_id, pending_steps, next_action), evidence_report\n"
|
|
244
|
+
f"Evidence required fields: patterns_checked, files_checked, commands_run, key_outputs, verbatim_outputs, cross_layer_impacts, open_gaps"
|
|
245
|
+
)
|
|
246
|
+
return ValidationResult(
|
|
247
|
+
is_valid=False,
|
|
248
|
+
missing=all_missing,
|
|
249
|
+
error_message=error_message,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ============================================================================
|
|
254
|
+
# Functions absorbed from evidence_parser.py (backward compatible)
|
|
255
|
+
# ============================================================================
|
|
256
|
+
|
|
257
|
+
def extract_commands_from_evidence(agent_output: str) -> List[str]:
|
|
258
|
+
"""Extract command strings from the EVIDENCE_REPORT COMMANDS_RUN field.
|
|
259
|
+
|
|
260
|
+
Only the ``json:contract`` fenced-block format is supported.
|
|
261
|
+
Extracts from ``evidence_report.commands_run`` list entries.
|
|
262
|
+
|
|
263
|
+
Commands whose result indicates they were NOT actually run (e.g. "not run",
|
|
264
|
+
"skipped", "n/a", "not executed") are excluded from the returned list.
|
|
265
|
+
|
|
266
|
+
Returns a list of command strings (without surrounding backticks).
|
|
267
|
+
"""
|
|
268
|
+
contract = parse_contract(agent_output)
|
|
269
|
+
if contract is None:
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
evidence = contract.get("evidence_report", {}) or {}
|
|
273
|
+
commands_run = evidence.get("commands_run", [])
|
|
274
|
+
if not isinstance(commands_run, list):
|
|
275
|
+
return []
|
|
276
|
+
|
|
277
|
+
commands: List[str] = []
|
|
278
|
+
for entry in commands_run:
|
|
279
|
+
if isinstance(entry, dict):
|
|
280
|
+
cmd = entry.get("command", entry.get("cmd", ""))
|
|
281
|
+
elif isinstance(entry, str):
|
|
282
|
+
cmd = entry
|
|
283
|
+
else:
|
|
284
|
+
continue
|
|
285
|
+
if cmd and cmd.lower() not in _LITERAL_NONE_COMMANDS:
|
|
286
|
+
if not _NOT_RUN_INDICATORS.search(cmd):
|
|
287
|
+
commands.append(cmd)
|
|
288
|
+
return commands
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def requires_consolidation_report(task_info: Dict[str, Any]) -> bool:
|
|
292
|
+
"""Determine whether runtime should require a CONSOLIDATION_REPORT block.
|
|
293
|
+
|
|
294
|
+
Checks injected_context for investigation_brief.consolidation_required,
|
|
295
|
+
investigation_brief.cross_check_required, or surface_routing.multi_surface.
|
|
296
|
+
|
|
297
|
+
Falls back to reading from the transcript if injected_context was not
|
|
298
|
+
pre-extracted.
|
|
299
|
+
"""
|
|
300
|
+
payload = task_info.get("injected_context") or {}
|
|
301
|
+
if not payload:
|
|
302
|
+
# Fallback: read from transcript if injected_context was not pre-extracted
|
|
303
|
+
from .transcript_reader import extract_injected_context_payload_from_transcript
|
|
304
|
+
payload = extract_injected_context_payload_from_transcript(
|
|
305
|
+
task_info.get("agent_transcript_path", "")
|
|
306
|
+
)
|
|
307
|
+
if not payload:
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
investigation_brief = payload.get("investigation_brief", {}) or {}
|
|
311
|
+
surface_routing = payload.get("surface_routing", {}) or {}
|
|
312
|
+
return bool(
|
|
313
|
+
investigation_brief.get("consolidation_required")
|
|
314
|
+
or investigation_brief.get("cross_check_required")
|
|
315
|
+
or surface_routing.get("multi_surface")
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def extract_plan_status_from_output(agent_output: str) -> str:
|
|
320
|
+
"""Extract the PLAN_STATUS string from agent output.
|
|
321
|
+
|
|
322
|
+
Only the ``json:contract`` fenced-block format is supported.
|
|
323
|
+
|
|
324
|
+
Returns the raw status string (e.g. "COMPLETE", "BLOCKED", "NEEDS_INPUT")
|
|
325
|
+
or empty string if not found.
|
|
326
|
+
"""
|
|
327
|
+
contract = parse_contract(agent_output)
|
|
328
|
+
if contract is None:
|
|
329
|
+
return ""
|
|
330
|
+
|
|
331
|
+
agent_status = contract.get("agent_status", {}) or {}
|
|
332
|
+
plan_status = agent_status.get("plan_status", "")
|
|
333
|
+
if plan_status:
|
|
334
|
+
return str(plan_status).upper().rstrip(".,;")
|
|
335
|
+
return ""
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def extract_exit_code_from_output(agent_output: str) -> int:
|
|
339
|
+
"""Derive exit code from the LAST AGENT_STATUS block in agent output.
|
|
340
|
+
|
|
341
|
+
Looks for PLAN_STATUS in the final assistant message. If the status
|
|
342
|
+
contains COMPLETE -> 0, BLOCKED or ERROR -> 1. Falls back to 0 when
|
|
343
|
+
no AGENT_STATUS is found (optimistic default).
|
|
344
|
+
"""
|
|
345
|
+
status_value = extract_plan_status_from_output(agent_output)
|
|
346
|
+
if status_value:
|
|
347
|
+
if "COMPLETE" in status_value:
|
|
348
|
+
return 0
|
|
349
|
+
if "BLOCKED" in status_value or "ERROR" in status_value:
|
|
350
|
+
return 1
|
|
351
|
+
return 0
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# ============================================================================
|
|
355
|
+
# Context-usage anomaly detection
|
|
356
|
+
# ============================================================================
|
|
357
|
+
|
|
358
|
+
# Reuse the anchor extraction regex from anchor_tracker for consistency
|
|
359
|
+
_ANCHOR_FIELDS_RE = re.compile(
|
|
360
|
+
r"(path|name|cluster|project|region|namespace|service|image|"
|
|
361
|
+
r"base_path|config_path|module_path|repository|bucket|sa$|"
|
|
362
|
+
r"service_account|pod_name|terragrunt_path)",
|
|
363
|
+
re.IGNORECASE,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
_MIN_ANCHOR_LEN = 4
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _extract_context_anchors(project_knowledge: Dict[str, Any]) -> set:
|
|
370
|
+
"""Extract anchor strings (paths, names, IDs) from project_knowledge sections.
|
|
371
|
+
|
|
372
|
+
Walks the project_knowledge dict and collects string values from fields
|
|
373
|
+
whose names match anchor-worthy patterns (paths, service names, clusters, etc.).
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
project_knowledge: The project_knowledge dict from the injected context.
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
Set of anchor strings.
|
|
380
|
+
"""
|
|
381
|
+
anchors: set = set()
|
|
382
|
+
|
|
383
|
+
def _walk(obj: Any, depth: int = 0) -> None:
|
|
384
|
+
if depth > 10:
|
|
385
|
+
return
|
|
386
|
+
if isinstance(obj, dict):
|
|
387
|
+
for key, value in obj.items():
|
|
388
|
+
if isinstance(value, str) and value and _ANCHOR_FIELDS_RE.search(key):
|
|
389
|
+
clean = value.lstrip("./")
|
|
390
|
+
if len(clean) >= _MIN_ANCHOR_LEN:
|
|
391
|
+
anchors.add(clean)
|
|
392
|
+
elif isinstance(value, (dict, list)):
|
|
393
|
+
_walk(value, depth + 1)
|
|
394
|
+
elif isinstance(obj, list):
|
|
395
|
+
for item in obj:
|
|
396
|
+
_walk(item, depth + 1)
|
|
397
|
+
|
|
398
|
+
_walk(project_knowledge)
|
|
399
|
+
return anchors
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def check_context_usage(
|
|
403
|
+
project_knowledge: Dict[str, Any],
|
|
404
|
+
evidence_report: Dict[str, Any],
|
|
405
|
+
) -> Dict[str, Any]:
|
|
406
|
+
"""Soft check: detect when an agent ignores injected project context.
|
|
407
|
+
|
|
408
|
+
Extracts anchors from project_knowledge and checks whether ANY of them
|
|
409
|
+
appear in the agent's evidence_report (files_checked, patterns_checked,
|
|
410
|
+
commands_run). If zero overlap, flags ``context_ignored: true``.
|
|
411
|
+
|
|
412
|
+
This is a soft check -- it never fails validation, only adds a flag.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
project_knowledge: The ``project_knowledge`` dict from injected context.
|
|
416
|
+
evidence_report: The ``evidence_report`` dict from the agent's json:contract.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Dict with ``context_ignored`` (bool), ``anchors_found`` (int),
|
|
420
|
+
``anchors_in_evidence`` (int), and ``overlap`` (list of matched anchors).
|
|
421
|
+
"""
|
|
422
|
+
if not project_knowledge or not evidence_report:
|
|
423
|
+
return {
|
|
424
|
+
"context_ignored": False,
|
|
425
|
+
"anchors_found": 0,
|
|
426
|
+
"anchors_in_evidence": 0,
|
|
427
|
+
"overlap": [],
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
anchors = _extract_context_anchors(project_knowledge)
|
|
431
|
+
if not anchors:
|
|
432
|
+
return {
|
|
433
|
+
"context_ignored": False,
|
|
434
|
+
"anchors_found": 0,
|
|
435
|
+
"anchors_in_evidence": 0,
|
|
436
|
+
"overlap": [],
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
# Build a single searchable string from evidence fields
|
|
440
|
+
evidence_parts: List[str] = []
|
|
441
|
+
|
|
442
|
+
for field in ("files_checked", "patterns_checked", "commands_run"):
|
|
443
|
+
entries = evidence_report.get(field, [])
|
|
444
|
+
if isinstance(entries, list):
|
|
445
|
+
for entry in entries:
|
|
446
|
+
if isinstance(entry, str):
|
|
447
|
+
evidence_parts.append(entry)
|
|
448
|
+
elif isinstance(entry, dict):
|
|
449
|
+
# commands_run may be dicts with "command" or "cmd" keys
|
|
450
|
+
evidence_parts.append(
|
|
451
|
+
entry.get("command", entry.get("cmd", str(entry)))
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
evidence_text = " ".join(evidence_parts)
|
|
455
|
+
|
|
456
|
+
matched: List[str] = []
|
|
457
|
+
for anchor in anchors:
|
|
458
|
+
if anchor in evidence_text:
|
|
459
|
+
matched.append(anchor)
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
"context_ignored": len(matched) == 0,
|
|
463
|
+
"anchors_found": len(anchors),
|
|
464
|
+
"anchors_in_evidence": len(matched),
|
|
465
|
+
"overlap": sorted(matched),
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
# ============================================================================
|
|
470
|
+
# Cross-field validation: verbatim_outputs consistency (Option D)
|
|
471
|
+
# ============================================================================
|
|
472
|
+
|
|
473
|
+
_VERBATIM_PLACEHOLDER_PATTERNS = re.compile(
|
|
474
|
+
r"^(N/?A|none|no\s+output|no\s+output\s+captured|not\s+applicable|"
|
|
475
|
+
r"no\s+commands?\s+run|no\s+verbatim\s+output|n/a|\[\]|-|"
|
|
476
|
+
r"no\s+output\s+to\s+capture|not\s+available)\.?$",
|
|
477
|
+
re.IGNORECASE,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _is_real_command(entry: str) -> bool:
|
|
482
|
+
"""Return True if the commands_run entry represents a real executed command."""
|
|
483
|
+
if not entry or not entry.strip():
|
|
484
|
+
return False
|
|
485
|
+
normalized = entry.strip().lower()
|
|
486
|
+
if normalized in _LITERAL_NONE_COMMANDS:
|
|
487
|
+
return False
|
|
488
|
+
if _NOT_RUN_INDICATORS.search(normalized):
|
|
489
|
+
return False
|
|
490
|
+
return True
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _is_placeholder_output(entry: str) -> bool:
|
|
494
|
+
"""Return True if the verbatim_outputs entry is a placeholder, not real output."""
|
|
495
|
+
if not entry or not entry.strip():
|
|
496
|
+
return True
|
|
497
|
+
return bool(_VERBATIM_PLACEHOLDER_PATTERNS.match(entry.strip()))
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def validate_verbatim_outputs_consistency(
|
|
501
|
+
parsed_contract: Optional[dict],
|
|
502
|
+
) -> Optional[Dict[str, Any]]:
|
|
503
|
+
"""Cross-field validation: commands_run vs verbatim_outputs.
|
|
504
|
+
|
|
505
|
+
If commands_run has 1+ real entries, verbatim_outputs must have at least 1
|
|
506
|
+
entry that is NOT a placeholder. Returns an anomaly dict if the check fails,
|
|
507
|
+
None if it passes or does not apply.
|
|
508
|
+
|
|
509
|
+
This is advisory only -- it should be logged but never block.
|
|
510
|
+
"""
|
|
511
|
+
if parsed_contract is None:
|
|
512
|
+
return None
|
|
513
|
+
|
|
514
|
+
evidence = parsed_contract.get("evidence_report")
|
|
515
|
+
if not evidence or not isinstance(evidence, dict):
|
|
516
|
+
return None
|
|
517
|
+
|
|
518
|
+
commands_run = evidence.get("commands_run", [])
|
|
519
|
+
if not isinstance(commands_run, list):
|
|
520
|
+
return None
|
|
521
|
+
|
|
522
|
+
# Count real commands
|
|
523
|
+
real_commands = []
|
|
524
|
+
for entry in commands_run:
|
|
525
|
+
if isinstance(entry, dict):
|
|
526
|
+
cmd = entry.get("command", entry.get("cmd", ""))
|
|
527
|
+
elif isinstance(entry, str):
|
|
528
|
+
cmd = entry
|
|
529
|
+
else:
|
|
530
|
+
continue
|
|
531
|
+
if _is_real_command(cmd):
|
|
532
|
+
real_commands.append(cmd)
|
|
533
|
+
|
|
534
|
+
if not real_commands:
|
|
535
|
+
return None # No real commands -- check does not apply
|
|
536
|
+
|
|
537
|
+
# Check verbatim_outputs for at least 1 non-placeholder entry
|
|
538
|
+
verbatim_outputs = evidence.get("verbatim_outputs", [])
|
|
539
|
+
if not isinstance(verbatim_outputs, list):
|
|
540
|
+
verbatim_outputs = []
|
|
541
|
+
|
|
542
|
+
has_real_output = False
|
|
543
|
+
for entry in verbatim_outputs:
|
|
544
|
+
text = ""
|
|
545
|
+
if isinstance(entry, str):
|
|
546
|
+
text = entry
|
|
547
|
+
elif isinstance(entry, dict):
|
|
548
|
+
text = entry.get("output", entry.get("content", str(entry)))
|
|
549
|
+
if text and not _is_placeholder_output(text):
|
|
550
|
+
has_real_output = True
|
|
551
|
+
break
|
|
552
|
+
|
|
553
|
+
if has_real_output:
|
|
554
|
+
return None # Passes -- real commands have backing output
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
"type": "verbatim_outputs_missing",
|
|
558
|
+
"severity": "warning",
|
|
559
|
+
"message": (
|
|
560
|
+
f"Agent ran {len(real_commands)} command(s) but verbatim_outputs "
|
|
561
|
+
f"contains no real output (only placeholders or empty). "
|
|
562
|
+
f"Commands: {', '.join(c[:60] for c in real_commands[:3])}"
|
|
563
|
+
),
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# ============================================================================
|
|
568
|
+
# False pending-approval detection
|
|
569
|
+
# ============================================================================
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
# ============================================================================
|
|
573
|
+
# Approval request validation
|
|
574
|
+
# ============================================================================
|
|
575
|
+
|
|
576
|
+
_APPROVAL_STATUSES = {"REVIEW"}
|
|
577
|
+
|
|
578
|
+
_APPROVAL_REQUIRED_FIELDS = [
|
|
579
|
+
"operation", "exact_content", "scope", "risk_level", "rollback", "verification",
|
|
580
|
+
]
|
|
581
|
+
|
|
582
|
+
_VALID_RISK_LEVELS = {"LOW", "MEDIUM", "HIGH", "CRITICAL"}
|
|
583
|
+
|
|
584
|
+
_NONCE_HEX_RE = re.compile(r"^[a-f0-9]{32}$")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def validate_approval_request(
|
|
588
|
+
contract: dict,
|
|
589
|
+
plan_status: str,
|
|
590
|
+
) -> Optional[Dict[str, Any]]:
|
|
591
|
+
"""Validate the approval_request block when plan_status is REVIEW.
|
|
592
|
+
|
|
593
|
+
Advisory only -- returns an anomaly dict if validation fails, None if OK
|
|
594
|
+
or if the check does not apply.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
contract: Parsed dict from parse_contract().
|
|
598
|
+
plan_status: The agent's reported plan_status string (already uppercased).
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
An anomaly dict (severity: info or warning) when the check triggers, None otherwise.
|
|
602
|
+
"""
|
|
603
|
+
if plan_status.upper() not in _APPROVAL_STATUSES:
|
|
604
|
+
return None
|
|
605
|
+
|
|
606
|
+
approval_req = contract.get("approval_request")
|
|
607
|
+
if not approval_req or not isinstance(approval_req, dict):
|
|
608
|
+
return {
|
|
609
|
+
"type": "approval_request_missing",
|
|
610
|
+
"severity": "info",
|
|
611
|
+
"detail": (
|
|
612
|
+
f"Agent returned {plan_status} without an approval_request block. "
|
|
613
|
+
f"Expected fields: {', '.join(_APPROVAL_REQUIRED_FIELDS)}"
|
|
614
|
+
),
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
missing_fields: List[str] = []
|
|
618
|
+
for field in _APPROVAL_REQUIRED_FIELDS:
|
|
619
|
+
if not approval_req.get(field):
|
|
620
|
+
missing_fields.append(field)
|
|
621
|
+
|
|
622
|
+
# Validate risk_level value if present
|
|
623
|
+
risk = str(approval_req.get("risk_level", "")).upper()
|
|
624
|
+
invalid_risk = risk and risk not in _VALID_RISK_LEVELS
|
|
625
|
+
|
|
626
|
+
nonce_issue = None
|
|
627
|
+
|
|
628
|
+
issues: List[str] = []
|
|
629
|
+
if missing_fields:
|
|
630
|
+
issues.append(f"missing fields: {', '.join(missing_fields)}")
|
|
631
|
+
if invalid_risk:
|
|
632
|
+
issues.append(f"invalid risk_level: {risk}")
|
|
633
|
+
if nonce_issue:
|
|
634
|
+
issues.append(nonce_issue)
|
|
635
|
+
|
|
636
|
+
if not issues:
|
|
637
|
+
return None
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
"type": "approval_request_incomplete",
|
|
641
|
+
"severity": "warning",
|
|
642
|
+
"detail": (
|
|
643
|
+
f"approval_request block for {plan_status} has issues: "
|
|
644
|
+
f"{'; '.join(issues)}"
|
|
645
|
+
),
|
|
646
|
+
"missing_fields": missing_fields,
|
|
647
|
+
}
|