@jaguilar87/gaia 5.0.0-rc.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 +33 -0
- package/.claude-plugin/plugin.json +26 -0
- package/ARCHITECTURE.md +335 -0
- package/CHANGELOG.md +1298 -0
- package/CODE_OF_CONDUCT.md +11 -0
- package/CONTRIBUTING.md +146 -0
- package/INSTALL.md +436 -0
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/SECURITY.md +47 -0
- package/agents/README.md +78 -0
- package/agents/cloud-troubleshooter.md +73 -0
- package/agents/developer.md +65 -0
- package/agents/gaia-operator.md +64 -0
- package/agents/gaia-orchestrator.md +111 -0
- package/agents/gaia-planner.md +53 -0
- package/agents/gaia-system.md +71 -0
- package/agents/gitops-operator.md +61 -0
- package/agents/terraform-architect.md +63 -0
- package/bin/README.md +106 -0
- package/bin/cli/__init__.py +1 -0
- package/bin/cli/approvals.py +740 -0
- package/bin/cli/cleanup.py +562 -0
- package/bin/cli/context.py +283 -0
- package/bin/cli/doctor.py +651 -0
- package/bin/cli/history.py +305 -0
- package/bin/cli/memory.py +483 -0
- package/bin/cli/metrics.py +1068 -0
- package/bin/cli/plans.py +515 -0
- package/bin/cli/status.py +302 -0
- package/bin/cli/update.py +382 -0
- package/bin/gaia +112 -0
- package/bin/gaia-cleanup.js +531 -0
- package/bin/gaia-doctor.js +635 -0
- package/bin/gaia-evidence +126 -0
- package/bin/gaia-history.js +251 -0
- package/bin/gaia-metrics.js +1278 -0
- package/bin/gaia-review.js +269 -0
- package/bin/gaia-scan +44 -0
- package/bin/gaia-scan.py +589 -0
- package/bin/gaia-skills-diagnose.js +929 -0
- package/bin/gaia-status.js +278 -0
- package/bin/gaia-uninstall.js +111 -0
- package/bin/gaia-update.js +919 -0
- package/bin/pre-publish-validate.js +610 -0
- package/bin/python-detect.js +60 -0
- package/bin/validate-sandbox.sh +601 -0
- package/commands/README.md +64 -0
- package/commands/gaia.md +37 -0
- package/commands/scan-project.md +67 -0
- package/config/README.md +71 -0
- package/config/cloud/aws.json +134 -0
- package/config/cloud/gcp.json +139 -0
- package/config/context-contracts.json +158 -0
- package/config/crons-schema.md +81 -0
- package/config/git_standards.json +72 -0
- package/config/surface-routing.json +417 -0
- package/config/universal-rules.json +102 -0
- package/dist/gaia-ops/.claude-plugin/plugin.json +24 -0
- package/dist/gaia-ops/README.md +80 -0
- package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
- package/dist/gaia-ops/agents/developer.md +65 -0
- package/dist/gaia-ops/agents/gaia-operator.md +64 -0
- package/dist/gaia-ops/agents/gaia-orchestrator.md +111 -0
- package/dist/gaia-ops/agents/gaia-planner.md +53 -0
- package/dist/gaia-ops/agents/gaia-system.md +71 -0
- package/dist/gaia-ops/agents/gitops-operator.md +61 -0
- package/dist/gaia-ops/agents/terraform-architect.md +63 -0
- package/dist/gaia-ops/commands/gaia.md +37 -0
- package/dist/gaia-ops/config/README.md +71 -0
- package/dist/gaia-ops/config/cloud/aws.json +134 -0
- package/dist/gaia-ops/config/cloud/gcp.json +139 -0
- package/dist/gaia-ops/config/context-contracts.json +158 -0
- package/dist/gaia-ops/config/crons-schema.md +81 -0
- package/dist/gaia-ops/config/git_standards.json +72 -0
- package/dist/gaia-ops/config/surface-routing.json +417 -0
- package/dist/gaia-ops/config/universal-rules.json +102 -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 +1890 -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 +192 -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 +120 -0
- package/dist/gaia-ops/hooks/modules/agents/state_tracker.py +267 -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 +611 -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/agentic_loop_detector.py +165 -0
- package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +218 -0
- package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-ops/hooks/modules/context/context_injector.py +558 -0
- package/dist/gaia-ops/hooks/modules/context/context_writer.py +530 -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 +577 -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/memory/__init__.py +8 -0
- package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +216 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +122 -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 +120 -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 +1638 -0
- package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +222 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +595 -0
- package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/dist/gaia-ops/hooks/modules/security/command_semantics.py +181 -0
- package/dist/gaia-ops/hooks/modules/security/composition_rules.py +547 -0
- package/dist/gaia-ops/hooks/modules/security/flag_classifiers.py +873 -0
- package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +1131 -0
- package/dist/gaia-ops/hooks/modules/security/network_hosts.py +481 -0
- package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-ops/hooks/modules/security/shell_unwrapper.py +165 -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/pending_scanner.py +174 -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 +160 -0
- package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-ops/hooks/modules/session/session_registry.py +333 -0
- package/dist/gaia-ops/hooks/modules/tools/__init__.py +29 -0
- package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +1008 -0
- package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +231 -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/stage_decomposer.py +315 -0
- package/dist/gaia-ops/hooks/modules/tools/task_validator.py +294 -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_compact.py +60 -0
- package/dist/gaia-ops/hooks/pre_tool_use.py +413 -0
- package/dist/gaia-ops/hooks/session_end_hook.py +77 -0
- package/dist/gaia-ops/hooks/session_start.py +81 -0
- package/dist/gaia-ops/hooks/stop_hook.py +70 -0
- package/dist/gaia-ops/hooks/subagent_start.py +71 -0
- package/dist/gaia-ops/hooks/subagent_stop.py +295 -0
- package/dist/gaia-ops/hooks/task_completed.py +70 -0
- package/dist/gaia-ops/hooks/user_prompt_submit.py +246 -0
- package/dist/gaia-ops/settings.json +72 -0
- package/dist/gaia-ops/skills/README.md +158 -0
- package/dist/gaia-ops/skills/agent-creation/SKILL.md +87 -0
- package/dist/gaia-ops/skills/agent-creation/examples.md +170 -0
- package/dist/gaia-ops/skills/agent-creation/reference.md +191 -0
- package/dist/gaia-ops/skills/agent-protocol/SKILL.md +93 -0
- package/dist/gaia-ops/skills/agent-protocol/examples.md +223 -0
- package/dist/gaia-ops/skills/agent-response/SKILL.md +69 -0
- package/dist/gaia-ops/skills/agentic-loop/SKILL.md +80 -0
- package/dist/gaia-ops/skills/agentic-loop/reference.md +378 -0
- package/dist/gaia-ops/skills/blog-writing/SKILL.md +98 -0
- package/dist/gaia-ops/skills/blog-writing/reference.md +130 -0
- package/dist/gaia-ops/skills/brief-spec/SKILL.md +185 -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 +87 -0
- package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
- package/dist/gaia-ops/skills/developer-patterns/SKILL.md +50 -0
- package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
- package/dist/gaia-ops/skills/execution/SKILL.md +99 -0
- package/dist/gaia-ops/skills/fast-queries/SKILL.md +43 -0
- package/dist/gaia-ops/skills/gaia-compact/SKILL.md +74 -0
- package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +108 -0
- package/dist/gaia-ops/skills/gaia-patterns/reference.md +395 -0
- package/dist/gaia-ops/skills/gaia-planner/SKILL.md +37 -0
- package/dist/gaia-ops/skills/gaia-planner/reference.md +107 -0
- package/dist/gaia-ops/skills/gaia-release/SKILL.md +85 -0
- package/dist/gaia-ops/skills/gaia-release/reference.md +92 -0
- package/dist/gaia-ops/skills/gaia-self-check/SKILL.md +114 -0
- package/dist/gaia-ops/skills/gaia-self-check/reference.md +453 -0
- package/dist/gaia-ops/skills/gaia-verify/SKILL.md +77 -0
- package/dist/gaia-ops/skills/gaia-verify/reference.md +80 -0
- package/dist/gaia-ops/skills/git-conventions/SKILL.md +47 -0
- package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +60 -0
- package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
- package/dist/gaia-ops/skills/gmail-policy/SKILL.md +200 -0
- package/dist/gaia-ops/skills/gmail-policy/reference.md +150 -0
- package/dist/gaia-ops/skills/gmail-triage/SKILL.md +100 -0
- package/dist/gaia-ops/skills/gws-setup/SKILL.md +99 -0
- package/dist/gaia-ops/skills/gws-setup/reference.md +73 -0
- package/dist/gaia-ops/skills/investigation/SKILL.md +100 -0
- package/dist/gaia-ops/skills/memory-curation/SKILL.md +83 -0
- package/dist/gaia-ops/skills/memory-search/SKILL.md +88 -0
- package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +160 -0
- package/dist/gaia-ops/skills/orchestrator-approval/reference.md +174 -0
- package/dist/gaia-ops/skills/pending-approvals/SKILL.md +72 -0
- package/dist/gaia-ops/skills/pending-approvals/reference.md +214 -0
- package/dist/gaia-ops/skills/readme-writing/SKILL.md +71 -0
- package/dist/gaia-ops/skills/readme-writing/reference.md +188 -0
- package/dist/gaia-ops/skills/reference.md +135 -0
- package/dist/gaia-ops/skills/request-approval/SKILL.md +140 -0
- package/dist/gaia-ops/skills/request-approval/examples.md +140 -0
- package/dist/gaia-ops/skills/request-approval/reference.md +57 -0
- package/dist/gaia-ops/skills/schedule-task/SKILL.md +64 -0
- package/dist/gaia-ops/skills/schedule-task/reference.md +233 -0
- package/dist/gaia-ops/skills/security-tiers/SKILL.md +141 -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/session-reflection/SKILL.md +69 -0
- package/dist/gaia-ops/skills/skill-creation/SKILL.md +92 -0
- package/dist/gaia-ops/skills/skill-creation/reference.md +29 -0
- package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +89 -0
- package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
- package/dist/gaia-ops/tools/__init__.py +9 -0
- package/dist/gaia-ops/tools/agentic-loop/decide-status.py +210 -0
- package/dist/gaia-ops/tools/agentic-loop/parse-metric.py +106 -0
- package/dist/gaia-ops/tools/agentic-loop/record-iteration.py +221 -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 +721 -0
- package/dist/gaia-ops/tools/context/context_section_reader.py +342 -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 +264 -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/backfill_fts5.py +107 -0
- package/dist/gaia-ops/tools/memory/conflict_detector.py +295 -0
- package/dist/gaia-ops/tools/memory/episodic.py +1210 -0
- package/dist/gaia-ops/tools/memory/git_invalidator.py +262 -0
- package/dist/gaia-ops/tools/memory/paths.py +102 -0
- package/dist/gaia-ops/tools/memory/scoring.py +193 -0
- package/dist/gaia-ops/tools/memory/search_store.py +375 -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 +349 -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 +686 -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 +270 -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 +24 -0
- package/dist/gaia-security/README.md +90 -0
- package/dist/gaia-security/config/universal-rules.json +102 -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 +1890 -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 +113 -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 +120 -0
- package/dist/gaia-security/hooks/modules/agents/state_tracker.py +267 -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 +611 -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/agentic_loop_detector.py +165 -0
- package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
- package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +218 -0
- package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
- package/dist/gaia-security/hooks/modules/context/context_injector.py +558 -0
- package/dist/gaia-security/hooks/modules/context/context_writer.py +530 -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 +577 -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/memory/__init__.py +8 -0
- package/dist/gaia-security/hooks/modules/memory/episode_writer.py +216 -0
- package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
- package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +122 -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 +120 -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 +1638 -0
- package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
- package/dist/gaia-security/hooks/modules/security/approval_scopes.py +222 -0
- package/dist/gaia-security/hooks/modules/security/blocked_commands.py +595 -0
- package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/dist/gaia-security/hooks/modules/security/command_semantics.py +181 -0
- package/dist/gaia-security/hooks/modules/security/composition_rules.py +547 -0
- package/dist/gaia-security/hooks/modules/security/flag_classifiers.py +873 -0
- package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
- package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +1131 -0
- package/dist/gaia-security/hooks/modules/security/network_hosts.py +481 -0
- package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
- package/dist/gaia-security/hooks/modules/security/shell_unwrapper.py +165 -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/pending_scanner.py +174 -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 +160 -0
- package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
- package/dist/gaia-security/hooks/modules/session/session_registry.py +333 -0
- package/dist/gaia-security/hooks/modules/tools/__init__.py +29 -0
- package/dist/gaia-security/hooks/modules/tools/bash_validator.py +1008 -0
- package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +231 -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/stage_decomposer.py +315 -0
- package/dist/gaia-security/hooks/modules/tools/task_validator.py +294 -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 +413 -0
- package/dist/gaia-security/hooks/session_end_hook.py +77 -0
- package/dist/gaia-security/hooks/session_start.py +81 -0
- package/dist/gaia-security/hooks/stop_hook.py +70 -0
- package/dist/gaia-security/hooks/user_prompt_submit.py +246 -0
- package/dist/gaia-security/settings.json +58 -0
- package/git-hooks/commit-msg +41 -0
- package/hooks/README.md +100 -0
- package/hooks/adapters/__init__.py +52 -0
- package/hooks/adapters/base.py +219 -0
- package/hooks/adapters/channel.py +17 -0
- package/hooks/adapters/claude_code.py +1890 -0
- package/hooks/adapters/types.py +194 -0
- package/hooks/adapters/utils.py +25 -0
- package/hooks/elicitation_result.py +179 -0
- package/hooks/hooks.json +84 -0
- package/hooks/modules/README.md +189 -0
- package/hooks/modules/__init__.py +15 -0
- package/hooks/modules/agents/__init__.py +29 -0
- package/hooks/modules/agents/contract_validator.py +647 -0
- package/hooks/modules/agents/response_contract.py +496 -0
- package/hooks/modules/agents/skill_injection_verifier.py +120 -0
- package/hooks/modules/agents/state_tracker.py +267 -0
- package/hooks/modules/agents/task_info_builder.py +74 -0
- package/hooks/modules/agents/transcript_analyzer.py +458 -0
- package/hooks/modules/agents/transcript_reader.py +152 -0
- package/hooks/modules/audit/__init__.py +28 -0
- package/hooks/modules/audit/event_detector.py +168 -0
- package/hooks/modules/audit/logger.py +131 -0
- package/hooks/modules/audit/metrics.py +134 -0
- package/hooks/modules/audit/workflow_auditor.py +611 -0
- package/hooks/modules/audit/workflow_recorder.py +296 -0
- package/hooks/modules/context/__init__.py +11 -0
- package/hooks/modules/context/agentic_loop_detector.py +165 -0
- package/hooks/modules/context/anchor_tracker.py +317 -0
- package/hooks/modules/context/compact_context_builder.py +218 -0
- package/hooks/modules/context/context_freshness.py +145 -0
- package/hooks/modules/context/context_injector.py +558 -0
- package/hooks/modules/context/context_writer.py +530 -0
- package/hooks/modules/context/contracts_loader.py +161 -0
- package/hooks/modules/core/__init__.py +40 -0
- package/hooks/modules/core/hook_entry.py +78 -0
- package/hooks/modules/core/paths.py +160 -0
- package/hooks/modules/core/plugin_mode.py +149 -0
- package/hooks/modules/core/plugin_setup.py +577 -0
- package/hooks/modules/core/state.py +179 -0
- package/hooks/modules/core/stdin.py +24 -0
- package/hooks/modules/events/__init__.py +1 -0
- package/hooks/modules/events/event_writer.py +210 -0
- package/hooks/modules/evidence/__init__.py +34 -0
- package/hooks/modules/evidence/assertions.py +137 -0
- package/hooks/modules/evidence/index_writer.py +57 -0
- package/hooks/modules/evidence/loader.py +126 -0
- package/hooks/modules/evidence/runner.py +241 -0
- package/hooks/modules/memory/__init__.py +8 -0
- package/hooks/modules/memory/episode_writer.py +216 -0
- package/hooks/modules/orchestrator/__init__.py +1 -0
- package/hooks/modules/orchestrator/delegate_mode.py +122 -0
- package/hooks/modules/scanning/__init__.py +8 -0
- package/hooks/modules/scanning/scan_trigger.py +84 -0
- package/hooks/modules/security/__init__.py +120 -0
- package/hooks/modules/security/approval_cleanup.py +87 -0
- package/hooks/modules/security/approval_constants.py +23 -0
- package/hooks/modules/security/approval_grants.py +1638 -0
- package/hooks/modules/security/approval_messages.py +71 -0
- package/hooks/modules/security/approval_scopes.py +222 -0
- package/hooks/modules/security/blocked_commands.py +595 -0
- package/hooks/modules/security/blocked_message_formatter.py +87 -0
- package/hooks/modules/security/command_semantics.py +181 -0
- package/hooks/modules/security/composition_rules.py +547 -0
- package/hooks/modules/security/flag_classifiers.py +873 -0
- package/hooks/modules/security/gitops_validator.py +179 -0
- package/hooks/modules/security/mutative_verbs.py +1131 -0
- package/hooks/modules/security/network_hosts.py +481 -0
- package/hooks/modules/security/prompt_validator.py +40 -0
- package/hooks/modules/security/shell_unwrapper.py +165 -0
- package/hooks/modules/security/tiers.py +196 -0
- package/hooks/modules/session/__init__.py +10 -0
- package/hooks/modules/session/pending_scanner.py +174 -0
- package/hooks/modules/session/session_context_writer.py +100 -0
- package/hooks/modules/session/session_event_injector.py +160 -0
- package/hooks/modules/session/session_manager.py +31 -0
- package/hooks/modules/session/session_registry.py +333 -0
- package/hooks/modules/tools/__init__.py +29 -0
- package/hooks/modules/tools/bash_validator.py +1008 -0
- package/hooks/modules/tools/cloud_pipe_validator.py +231 -0
- package/hooks/modules/tools/hook_response.py +55 -0
- package/hooks/modules/tools/shell_parser.py +227 -0
- package/hooks/modules/tools/stage_decomposer.py +315 -0
- package/hooks/modules/tools/task_validator.py +294 -0
- package/hooks/modules/validation/__init__.py +23 -0
- package/hooks/modules/validation/commit_validator.py +380 -0
- package/hooks/post_compact.py +43 -0
- package/hooks/post_tool_use.py +54 -0
- package/hooks/pre_compact.py +60 -0
- package/hooks/pre_tool_use.py +413 -0
- package/hooks/session_end_hook.py +77 -0
- package/hooks/session_start.py +81 -0
- package/hooks/stop_hook.py +70 -0
- package/hooks/subagent_start.py +71 -0
- package/hooks/subagent_stop.py +295 -0
- package/hooks/task_completed.py +70 -0
- package/hooks/user_prompt_submit.py +246 -0
- package/index.js +83 -0
- package/package.json +103 -0
- package/pyproject.toml +32 -0
- package/skills/README.md +158 -0
- package/skills/agent-creation/SKILL.md +87 -0
- package/skills/agent-creation/examples.md +170 -0
- package/skills/agent-creation/reference.md +191 -0
- package/skills/agent-protocol/SKILL.md +93 -0
- package/skills/agent-protocol/examples.md +223 -0
- package/skills/agent-response/SKILL.md +69 -0
- package/skills/agentic-loop/SKILL.md +80 -0
- package/skills/agentic-loop/reference.md +378 -0
- package/skills/blog-writing/SKILL.md +98 -0
- package/skills/blog-writing/reference.md +130 -0
- package/skills/brief-spec/SKILL.md +185 -0
- package/skills/command-execution/SKILL.md +64 -0
- package/skills/command-execution/reference.md +83 -0
- package/skills/context-updater/SKILL.md +87 -0
- package/skills/context-updater/examples.md +71 -0
- package/skills/developer-patterns/SKILL.md +50 -0
- package/skills/developer-patterns/reference.md +112 -0
- package/skills/execution/SKILL.md +99 -0
- package/skills/fast-queries/SKILL.md +43 -0
- package/skills/gaia-compact/SKILL.md +74 -0
- package/skills/gaia-patterns/SKILL.md +108 -0
- package/skills/gaia-patterns/reference.md +395 -0
- package/skills/gaia-planner/SKILL.md +37 -0
- package/skills/gaia-planner/reference.md +107 -0
- package/skills/gaia-release/SKILL.md +85 -0
- package/skills/gaia-release/reference.md +92 -0
- package/skills/gaia-self-check/SKILL.md +114 -0
- package/skills/gaia-self-check/reference.md +453 -0
- package/skills/gaia-verify/SKILL.md +77 -0
- package/skills/gaia-verify/reference.md +80 -0
- package/skills/git-conventions/SKILL.md +47 -0
- package/skills/gitops-patterns/SKILL.md +60 -0
- package/skills/gitops-patterns/reference.md +183 -0
- package/skills/gmail-policy/SKILL.md +200 -0
- package/skills/gmail-policy/reference.md +150 -0
- package/skills/gmail-triage/SKILL.md +100 -0
- package/skills/gws-setup/SKILL.md +99 -0
- package/skills/gws-setup/reference.md +73 -0
- package/skills/investigation/SKILL.md +100 -0
- package/skills/memory-curation/SKILL.md +83 -0
- package/skills/memory-search/SKILL.md +88 -0
- package/skills/orchestrator-approval/SKILL.md +160 -0
- package/skills/orchestrator-approval/reference.md +174 -0
- package/skills/pending-approvals/SKILL.md +72 -0
- package/skills/pending-approvals/reference.md +214 -0
- package/skills/readme-writing/SKILL.md +71 -0
- package/skills/readme-writing/reference.md +188 -0
- package/skills/reference.md +135 -0
- package/skills/request-approval/SKILL.md +140 -0
- package/skills/request-approval/examples.md +140 -0
- package/skills/request-approval/reference.md +57 -0
- package/skills/schedule-task/SKILL.md +64 -0
- package/skills/schedule-task/reference.md +233 -0
- package/skills/security-tiers/SKILL.md +141 -0
- package/skills/security-tiers/destructive-commands-reference.md +623 -0
- package/skills/security-tiers/reference.md +39 -0
- package/skills/session-reflection/SKILL.md +69 -0
- package/skills/skill-creation/SKILL.md +92 -0
- package/skills/skill-creation/reference.md +29 -0
- package/skills/terraform-patterns/SKILL.md +89 -0
- package/skills/terraform-patterns/reference.md +93 -0
- package/templates/README.md +69 -0
- package/templates/managed-settings.template.json +43 -0
- package/tools/__init__.py +9 -0
- package/tools/agentic-loop/decide-status.py +210 -0
- package/tools/agentic-loop/parse-metric.py +106 -0
- package/tools/agentic-loop/record-iteration.py +221 -0
- package/tools/context/README.md +132 -0
- package/tools/context/__init__.py +42 -0
- package/tools/context/_paths.py +20 -0
- package/tools/context/context_provider.py +721 -0
- package/tools/context/context_section_reader.py +342 -0
- package/tools/context/deep_merge.py +159 -0
- package/tools/context/pending_updates.py +760 -0
- package/tools/context/surface_router.py +278 -0
- package/tools/fast-queries/README.md +65 -0
- package/tools/fast-queries/__init__.py +30 -0
- package/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
- package/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
- package/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
- package/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
- package/tools/fast-queries/run_triage.sh +59 -0
- package/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
- package/tools/gaia_simulator/__init__.py +33 -0
- package/tools/gaia_simulator/cli.py +354 -0
- package/tools/gaia_simulator/extractor.py +457 -0
- package/tools/gaia_simulator/reporter.py +258 -0
- package/tools/gaia_simulator/routing_simulator.py +334 -0
- package/tools/gaia_simulator/runner.py +539 -0
- package/tools/gaia_simulator/skills_mapper.py +264 -0
- package/tools/memory/README.md +0 -0
- package/tools/memory/__init__.py +20 -0
- package/tools/memory/backfill_fts5.py +107 -0
- package/tools/memory/conflict_detector.py +295 -0
- package/tools/memory/episodic.py +1210 -0
- package/tools/memory/git_invalidator.py +262 -0
- package/tools/memory/paths.py +102 -0
- package/tools/memory/scoring.py +193 -0
- package/tools/memory/search_store.py +375 -0
- package/tools/persist_transcript_analysis.py +85 -0
- package/tools/review/__init__.py +1 -0
- package/tools/review/review_engine.py +157 -0
- package/tools/scan/__init__.py +35 -0
- package/tools/scan/config.py +247 -0
- package/tools/scan/merge.py +212 -0
- package/tools/scan/orchestrator.py +549 -0
- package/tools/scan/registry.py +127 -0
- package/tools/scan/scanners/__init__.py +18 -0
- package/tools/scan/scanners/base.py +137 -0
- package/tools/scan/scanners/environment.py +349 -0
- package/tools/scan/scanners/git.py +570 -0
- package/tools/scan/scanners/infrastructure.py +875 -0
- package/tools/scan/scanners/orchestration.py +600 -0
- package/tools/scan/scanners/stack.py +1085 -0
- package/tools/scan/scanners/tools.py +260 -0
- package/tools/scan/setup.py +686 -0
- package/tools/scan/tests/__init__.py +1 -0
- package/tools/scan/tests/conftest.py +796 -0
- package/tools/scan/tests/test_environment.py +323 -0
- package/tools/scan/tests/test_git.py +419 -0
- package/tools/scan/tests/test_infrastructure.py +382 -0
- package/tools/scan/tests/test_integration.py +920 -0
- package/tools/scan/tests/test_merge.py +269 -0
- package/tools/scan/tests/test_orchestration.py +304 -0
- package/tools/scan/tests/test_stack.py +604 -0
- package/tools/scan/tests/test_tools.py +349 -0
- package/tools/scan/ui.py +624 -0
- package/tools/scan/verify.py +270 -0
- package/tools/scan/walk.py +118 -0
- package/tools/scan/workspace.py +85 -0
- package/tools/validation/README.md +244 -0
- package/tools/validation/__init__.py +17 -0
- package/tools/validation/approval_gate.py +321 -0
- package/tools/validation/validate_skills.py +189 -0
|
@@ -0,0 +1,873 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flag-dependent command classifiers for 15 command families.
|
|
3
|
+
|
|
4
|
+
This module runs in the classify phase BEFORE detect_mutative_command(). When a
|
|
5
|
+
classifier returns a FlagClassifierResult, it overrides verb-based classification.
|
|
6
|
+
When it returns None, the caller falls through to the existing mutative_verbs pipeline.
|
|
7
|
+
|
|
8
|
+
Classification outcomes:
|
|
9
|
+
READ_ONLY -- safe by elimination, no approval required
|
|
10
|
+
MUTATIVE -- state-modifying, requires user approval (T3 nonce)
|
|
11
|
+
BLOCKED -- permanently blocked (exit 2), maps to the same path as blocked_commands
|
|
12
|
+
|
|
13
|
+
Note on BLOCKED overlap with blocked_commands.py:
|
|
14
|
+
blocked_commands.py already permanently blocks:
|
|
15
|
+
- git push --force / -f
|
|
16
|
+
- git reset --hard
|
|
17
|
+
The classifiers for git push and git reset are still present here for
|
|
18
|
+
consistency (they return BLOCKED with the same reason), but blocked_commands.py
|
|
19
|
+
will catch these first in the pipeline. Having both layers is intentional:
|
|
20
|
+
flag_classifiers is the semantic-aware layer; blocked_commands is the
|
|
21
|
+
pattern-level safety net.
|
|
22
|
+
|
|
23
|
+
Dependencies: Python stdlib only.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import re
|
|
29
|
+
import shlex
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from typing import Callable, Dict, List, Optional, Tuple
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Outcome constants
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
OUTCOME_READ_ONLY = "READ_ONLY"
|
|
39
|
+
OUTCOME_MUTATIVE = "MUTATIVE"
|
|
40
|
+
OUTCOME_BLOCKED = "BLOCKED"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
# Result dataclass
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class FlagClassifierResult:
|
|
49
|
+
"""Structured result of flag-dependent classification.
|
|
50
|
+
|
|
51
|
+
Attributes:
|
|
52
|
+
outcome: One of OUTCOME_READ_ONLY, OUTCOME_MUTATIVE, or OUTCOME_BLOCKED.
|
|
53
|
+
reason: Human-readable explanation.
|
|
54
|
+
matched_pattern: The specific flag or pattern that triggered this
|
|
55
|
+
classification (e.g. "--force", "-i", "-exec").
|
|
56
|
+
command_family: The command family that handled classification
|
|
57
|
+
(e.g. "git_push", "sed", "curl").
|
|
58
|
+
"""
|
|
59
|
+
outcome: str
|
|
60
|
+
reason: str
|
|
61
|
+
matched_pattern: str
|
|
62
|
+
command_family: str
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def is_blocked(self) -> bool:
|
|
66
|
+
return self.outcome == OUTCOME_BLOCKED
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def is_mutative(self) -> bool:
|
|
70
|
+
return self.outcome in (OUTCOME_MUTATIVE, OUTCOME_BLOCKED)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def is_read_only(self) -> bool:
|
|
74
|
+
return self.outcome == OUTCOME_READ_ONLY
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Token helpers
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
def _tokenize(command: str) -> List[str]:
|
|
82
|
+
"""Tokenize a shell command using shlex, fallback to split on error."""
|
|
83
|
+
if not command or not command.strip():
|
|
84
|
+
return []
|
|
85
|
+
try:
|
|
86
|
+
return shlex.split(command.strip())
|
|
87
|
+
except ValueError:
|
|
88
|
+
return command.strip().split()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _has_flag(args: List[str], *flags: str) -> Optional[str]:
|
|
92
|
+
"""Return the first matching flag found in args, or None."""
|
|
93
|
+
flag_set = set(flags)
|
|
94
|
+
for a in args:
|
|
95
|
+
if a in flag_set:
|
|
96
|
+
return a
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _has_short_flag(args: List[str], letter: str) -> bool:
|
|
101
|
+
"""Return True if args contain a bundled or standalone short flag.
|
|
102
|
+
|
|
103
|
+
Handles both standalone ("-f") and clustered ("-xf", "-fx") forms.
|
|
104
|
+
"""
|
|
105
|
+
needle = f"-{letter}"
|
|
106
|
+
for a in args:
|
|
107
|
+
if a == needle:
|
|
108
|
+
return True
|
|
109
|
+
# Bundled short flags: -xvf contains 'f'
|
|
110
|
+
if len(a) >= 2 and a[0] == "-" and a[1] != "-":
|
|
111
|
+
if letter in a[1:]:
|
|
112
|
+
return True
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Individual classifiers (one per command family)
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
# Each classifier receives (tokens: List[str]) and returns
|
|
120
|
+
# Optional[FlagClassifierResult]. tokens[0] is the base command (or
|
|
121
|
+
# "git" for git sub-commands).
|
|
122
|
+
#
|
|
123
|
+
# Convention:
|
|
124
|
+
# - The function is named _classify_<family>
|
|
125
|
+
# - It is registered in _CLASSIFIER_REGISTRY below
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# 1. git push
|
|
130
|
+
def _classify_git_push(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
131
|
+
if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "push":
|
|
132
|
+
return None
|
|
133
|
+
args = tokens[2:]
|
|
134
|
+
|
|
135
|
+
# Force push / history rewrite forms (also caught by blocked_commands.py)
|
|
136
|
+
force_flag = _has_flag(args, "--force", "--mirror", "--prune", "--delete", "-d")
|
|
137
|
+
if force_flag:
|
|
138
|
+
return FlagClassifierResult(
|
|
139
|
+
outcome=OUTCOME_BLOCKED,
|
|
140
|
+
reason=f"git push {force_flag} rewrites/destroys remote history; use --force-with-lease",
|
|
141
|
+
matched_pattern=force_flag,
|
|
142
|
+
command_family="git_push",
|
|
143
|
+
)
|
|
144
|
+
if _has_short_flag(args, "f"):
|
|
145
|
+
return FlagClassifierResult(
|
|
146
|
+
outcome=OUTCOME_BLOCKED,
|
|
147
|
+
reason="git push -f rewrites remote history; use --force-with-lease",
|
|
148
|
+
matched_pattern="-f",
|
|
149
|
+
command_family="git_push",
|
|
150
|
+
)
|
|
151
|
+
# +refspec or :refspec
|
|
152
|
+
for a in args:
|
|
153
|
+
if (a.startswith("+") or a.startswith(":")) and len(a) > 1:
|
|
154
|
+
return FlagClassifierResult(
|
|
155
|
+
outcome=OUTCOME_BLOCKED,
|
|
156
|
+
reason=f"git push {a!r} force-pushes or deletes a remote ref",
|
|
157
|
+
matched_pattern=a,
|
|
158
|
+
command_family="git_push",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Plain push: mutative (needs T3 approval)
|
|
162
|
+
return FlagClassifierResult(
|
|
163
|
+
outcome=OUTCOME_MUTATIVE,
|
|
164
|
+
reason="git push modifies the remote repository",
|
|
165
|
+
matched_pattern="git push",
|
|
166
|
+
command_family="git_push",
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# 2. git reset
|
|
171
|
+
def _classify_git_reset(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
172
|
+
if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "reset":
|
|
173
|
+
return None
|
|
174
|
+
args = tokens[2:]
|
|
175
|
+
|
|
176
|
+
if "--hard" in args:
|
|
177
|
+
return FlagClassifierResult(
|
|
178
|
+
outcome=OUTCOME_BLOCKED,
|
|
179
|
+
reason="git reset --hard permanently discards uncommitted changes",
|
|
180
|
+
matched_pattern="--hard",
|
|
181
|
+
command_family="git_reset",
|
|
182
|
+
)
|
|
183
|
+
# --soft and --mixed are recoverable rewrites
|
|
184
|
+
return FlagClassifierResult(
|
|
185
|
+
outcome=OUTCOME_MUTATIVE,
|
|
186
|
+
reason="git reset modifies HEAD or the index",
|
|
187
|
+
matched_pattern="git reset",
|
|
188
|
+
command_family="git_reset",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# 3. git checkout
|
|
193
|
+
def _classify_git_checkout(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
194
|
+
if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "checkout":
|
|
195
|
+
return None
|
|
196
|
+
args = tokens[2:]
|
|
197
|
+
|
|
198
|
+
_DISCARD_FLAGS = {".", "--", "HEAD", "--force", "-f", "--ours", "--theirs"}
|
|
199
|
+
flag = _has_flag(args, *_DISCARD_FLAGS)
|
|
200
|
+
if flag:
|
|
201
|
+
return FlagClassifierResult(
|
|
202
|
+
outcome=OUTCOME_BLOCKED,
|
|
203
|
+
reason=f"git checkout {flag} discards uncommitted changes",
|
|
204
|
+
matched_pattern=flag,
|
|
205
|
+
command_family="git_checkout",
|
|
206
|
+
)
|
|
207
|
+
if _has_short_flag(args, "f"):
|
|
208
|
+
return FlagClassifierResult(
|
|
209
|
+
outcome=OUTCOME_BLOCKED,
|
|
210
|
+
reason="git checkout -f discards uncommitted changes",
|
|
211
|
+
matched_pattern="-f",
|
|
212
|
+
command_family="git_checkout",
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return FlagClassifierResult(
|
|
216
|
+
outcome=OUTCOME_MUTATIVE,
|
|
217
|
+
reason="git checkout switches branches or restores files",
|
|
218
|
+
matched_pattern="git checkout",
|
|
219
|
+
command_family="git_checkout",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# 4. git stash
|
|
224
|
+
def _classify_git_stash(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
225
|
+
if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "stash":
|
|
226
|
+
return None
|
|
227
|
+
# No sub-command = implicit "push"
|
|
228
|
+
args = tokens[2:]
|
|
229
|
+
sub = args[0].lower() if args else "push"
|
|
230
|
+
|
|
231
|
+
if sub in ("drop", "clear"):
|
|
232
|
+
return FlagClassifierResult(
|
|
233
|
+
outcome=OUTCOME_BLOCKED,
|
|
234
|
+
reason=f"git stash {sub} permanently removes stashed changes",
|
|
235
|
+
matched_pattern=sub,
|
|
236
|
+
command_family="git_stash",
|
|
237
|
+
)
|
|
238
|
+
if sub in ("list", "show"):
|
|
239
|
+
return FlagClassifierResult(
|
|
240
|
+
outcome=OUTCOME_READ_ONLY,
|
|
241
|
+
reason=f"git stash {sub} is read-only",
|
|
242
|
+
matched_pattern=sub,
|
|
243
|
+
command_family="git_stash",
|
|
244
|
+
)
|
|
245
|
+
# push, pop, apply, branch, save
|
|
246
|
+
return FlagClassifierResult(
|
|
247
|
+
outcome=OUTCOME_MUTATIVE,
|
|
248
|
+
reason=f"git stash {sub} modifies the stash or working tree",
|
|
249
|
+
matched_pattern=sub,
|
|
250
|
+
command_family="git_stash",
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
# 5. git rebase
|
|
255
|
+
def _classify_git_rebase(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
256
|
+
if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "rebase":
|
|
257
|
+
return None
|
|
258
|
+
args = tokens[2:]
|
|
259
|
+
|
|
260
|
+
if "--abort" in args:
|
|
261
|
+
return FlagClassifierResult(
|
|
262
|
+
outcome=OUTCOME_READ_ONLY,
|
|
263
|
+
reason="git rebase --abort cancels in-progress rebase without modifying history",
|
|
264
|
+
matched_pattern="--abort",
|
|
265
|
+
command_family="git_rebase",
|
|
266
|
+
)
|
|
267
|
+
if "--continue" in args or "--skip" in args:
|
|
268
|
+
return FlagClassifierResult(
|
|
269
|
+
outcome=OUTCOME_MUTATIVE,
|
|
270
|
+
reason="git rebase --continue/--skip advances an in-progress rebase",
|
|
271
|
+
matched_pattern="--continue" if "--continue" in args else "--skip",
|
|
272
|
+
command_family="git_rebase",
|
|
273
|
+
)
|
|
274
|
+
if "-i" in args or "--interactive" in args:
|
|
275
|
+
return FlagClassifierResult(
|
|
276
|
+
outcome=OUTCOME_MUTATIVE,
|
|
277
|
+
reason="git rebase -i rewrites commit history interactively",
|
|
278
|
+
matched_pattern="-i" if "-i" in args else "--interactive",
|
|
279
|
+
command_family="git_rebase",
|
|
280
|
+
)
|
|
281
|
+
# Plain rebase
|
|
282
|
+
return FlagClassifierResult(
|
|
283
|
+
outcome=OUTCOME_MUTATIVE,
|
|
284
|
+
reason="git rebase rewrites commit history",
|
|
285
|
+
matched_pattern="git rebase",
|
|
286
|
+
command_family="git_rebase",
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# 6. git tag
|
|
291
|
+
def _classify_git_tag(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
292
|
+
if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "tag":
|
|
293
|
+
return None
|
|
294
|
+
args = tokens[2:]
|
|
295
|
+
|
|
296
|
+
if not args:
|
|
297
|
+
return FlagClassifierResult(
|
|
298
|
+
outcome=OUTCOME_READ_ONLY,
|
|
299
|
+
reason="git tag with no arguments lists tags",
|
|
300
|
+
matched_pattern="git tag",
|
|
301
|
+
command_family="git_tag",
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Delete or force -- blocked
|
|
305
|
+
has_force = "--force" in args or _has_short_flag(args, "f")
|
|
306
|
+
has_delete = "--delete" in args or _has_short_flag(args, "d")
|
|
307
|
+
if has_force:
|
|
308
|
+
return FlagClassifierResult(
|
|
309
|
+
outcome=OUTCOME_BLOCKED,
|
|
310
|
+
reason="git tag --force rewrites an existing tag",
|
|
311
|
+
matched_pattern="--force",
|
|
312
|
+
command_family="git_tag",
|
|
313
|
+
)
|
|
314
|
+
if has_delete:
|
|
315
|
+
return FlagClassifierResult(
|
|
316
|
+
outcome=OUTCOME_BLOCKED,
|
|
317
|
+
reason="git tag --delete removes a tag",
|
|
318
|
+
matched_pattern="--delete",
|
|
319
|
+
command_family="git_tag",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Listing / verification flags
|
|
323
|
+
_LIST_FLAGS = {"-l", "--list", "-v", "--verify", "--contains", "--no-contains",
|
|
324
|
+
"--merged", "--no-merged", "--points-at"}
|
|
325
|
+
if any(a in _LIST_FLAGS or a.startswith("-n") for a in args):
|
|
326
|
+
return FlagClassifierResult(
|
|
327
|
+
outcome=OUTCOME_READ_ONLY,
|
|
328
|
+
reason="git tag with listing/verification flags is read-only",
|
|
329
|
+
matched_pattern=next(
|
|
330
|
+
(a for a in args if a in _LIST_FLAGS or a.startswith("-n")), "-l"
|
|
331
|
+
),
|
|
332
|
+
command_family="git_tag",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Creating a tag
|
|
336
|
+
return FlagClassifierResult(
|
|
337
|
+
outcome=OUTCOME_MUTATIVE,
|
|
338
|
+
reason="git tag creates a new tag",
|
|
339
|
+
matched_pattern="git tag",
|
|
340
|
+
command_family="git_tag",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# 7. git clean
|
|
345
|
+
def _classify_git_clean(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
346
|
+
if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "clean":
|
|
347
|
+
return None
|
|
348
|
+
args = tokens[2:]
|
|
349
|
+
|
|
350
|
+
if "--dry-run" in args or _has_short_flag(args, "n"):
|
|
351
|
+
return FlagClassifierResult(
|
|
352
|
+
outcome=OUTCOME_READ_ONLY,
|
|
353
|
+
reason="git clean --dry-run/-n shows what would be removed without deleting",
|
|
354
|
+
matched_pattern="--dry-run" if "--dry-run" in args else "-n",
|
|
355
|
+
command_family="git_clean",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# All other forms are destructive (removes untracked files)
|
|
359
|
+
return FlagClassifierResult(
|
|
360
|
+
outcome=OUTCOME_BLOCKED,
|
|
361
|
+
reason="git clean permanently deletes untracked files; use --dry-run first",
|
|
362
|
+
matched_pattern="git clean",
|
|
363
|
+
command_family="git_clean",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# 8. git remote
|
|
368
|
+
def _classify_git_remote(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
369
|
+
if len(tokens) < 2 or tokens[0] != "git" or tokens[1] != "remote":
|
|
370
|
+
return None
|
|
371
|
+
args = tokens[2:]
|
|
372
|
+
sub = args[0].lower() if args else ""
|
|
373
|
+
|
|
374
|
+
if sub in ("remove", "rm", "rename", "set-url", "set-head", "set-branches"):
|
|
375
|
+
return FlagClassifierResult(
|
|
376
|
+
outcome=OUTCOME_MUTATIVE,
|
|
377
|
+
reason=f"git remote {sub} modifies remote configuration",
|
|
378
|
+
matched_pattern=sub,
|
|
379
|
+
command_family="git_remote",
|
|
380
|
+
)
|
|
381
|
+
if sub in ("show", "get-url", ""):
|
|
382
|
+
return FlagClassifierResult(
|
|
383
|
+
outcome=OUTCOME_READ_ONLY,
|
|
384
|
+
reason=f"git remote {sub or '(list)'} is read-only",
|
|
385
|
+
matched_pattern=sub or "git remote",
|
|
386
|
+
command_family="git_remote",
|
|
387
|
+
)
|
|
388
|
+
# "add" is mutative
|
|
389
|
+
if sub == "add":
|
|
390
|
+
return FlagClassifierResult(
|
|
391
|
+
outcome=OUTCOME_MUTATIVE,
|
|
392
|
+
reason="git remote add registers a new remote",
|
|
393
|
+
matched_pattern="add",
|
|
394
|
+
command_family="git_remote",
|
|
395
|
+
)
|
|
396
|
+
if sub in ("-v", "--verbose"):
|
|
397
|
+
return FlagClassifierResult(
|
|
398
|
+
outcome=OUTCOME_READ_ONLY,
|
|
399
|
+
reason="git remote -v lists remotes",
|
|
400
|
+
matched_pattern=sub,
|
|
401
|
+
command_family="git_remote",
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Unknown sub-command -- fall through
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# 9. sed
|
|
409
|
+
def _classify_sed(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
410
|
+
if not tokens or tokens[0] != "sed":
|
|
411
|
+
return None
|
|
412
|
+
args = tokens[1:]
|
|
413
|
+
|
|
414
|
+
# -i / -I / --in-place mean in-place file editing
|
|
415
|
+
flag = _has_flag(args, "-i", "-I", "--in-place")
|
|
416
|
+
if flag:
|
|
417
|
+
return FlagClassifierResult(
|
|
418
|
+
outcome=OUTCOME_MUTATIVE,
|
|
419
|
+
reason=f"sed {flag} edits files in-place",
|
|
420
|
+
matched_pattern=flag,
|
|
421
|
+
command_family="sed",
|
|
422
|
+
)
|
|
423
|
+
# Bundled short flags: -ni (where i is in-place), -in, etc.
|
|
424
|
+
# Also handle -i.bak form (flag with inline backup suffix)
|
|
425
|
+
for a in args:
|
|
426
|
+
if a.startswith("-i") and a != "-i" and not a.startswith("--"):
|
|
427
|
+
# -i.bak, -ibak, etc. -- sed in-place with backup suffix
|
|
428
|
+
return FlagClassifierResult(
|
|
429
|
+
outcome=OUTCOME_MUTATIVE,
|
|
430
|
+
reason=f"sed {a} edits files in-place (with backup suffix)",
|
|
431
|
+
matched_pattern=a,
|
|
432
|
+
command_family="sed",
|
|
433
|
+
)
|
|
434
|
+
if len(a) >= 2 and a[0] == "-" and a[1] != "-" and "i" in a[1:]:
|
|
435
|
+
return FlagClassifierResult(
|
|
436
|
+
outcome=OUTCOME_MUTATIVE,
|
|
437
|
+
reason=f"sed {a} contains -i (in-place editing)",
|
|
438
|
+
matched_pattern=a,
|
|
439
|
+
command_family="sed",
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
return FlagClassifierResult(
|
|
443
|
+
outcome=OUTCOME_READ_ONLY,
|
|
444
|
+
reason="sed without -i writes to stdout, does not modify files",
|
|
445
|
+
matched_pattern="sed",
|
|
446
|
+
command_family="sed",
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# 10. awk
|
|
451
|
+
# Pattern matching for awk programs that perform side-effecting operations.
|
|
452
|
+
_AWK_MUTATIVE_PATTERNS = re.compile(
|
|
453
|
+
r"""
|
|
454
|
+
system\s*\( # system("cmd")
|
|
455
|
+
| \|\s*getline # pipe into getline
|
|
456
|
+
| print\s.*> # print expr > file (redirect)
|
|
457
|
+
| print\s.*>> # print expr >> file (append)
|
|
458
|
+
| close\s*\( # close(file/pipe) implies file I/O
|
|
459
|
+
| \|& # two-way pipe (gawk)
|
|
460
|
+
""",
|
|
461
|
+
re.VERBOSE,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _classify_awk(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
466
|
+
if not tokens or tokens[0] not in ("awk", "gawk", "mawk", "nawk"):
|
|
467
|
+
return None
|
|
468
|
+
|
|
469
|
+
# Scan all tokens for the awk program text (first non-flag non-value argument)
|
|
470
|
+
args = tokens[1:]
|
|
471
|
+
i = 0
|
|
472
|
+
while i < len(args):
|
|
473
|
+
a = args[i]
|
|
474
|
+
# Flags that take a value argument: -F, -v, -f, etc.
|
|
475
|
+
if a in ("-F", "-v", "-f", "-i", "-l", "-M", "-m", "-o"):
|
|
476
|
+
i += 2
|
|
477
|
+
continue
|
|
478
|
+
if a.startswith("-") and not a.startswith("--"):
|
|
479
|
+
i += 1
|
|
480
|
+
continue
|
|
481
|
+
if a == "--":
|
|
482
|
+
i += 1
|
|
483
|
+
break
|
|
484
|
+
# First non-flag argument is the program
|
|
485
|
+
program = a
|
|
486
|
+
m = _AWK_MUTATIVE_PATTERNS.search(program)
|
|
487
|
+
if m:
|
|
488
|
+
matched = m.group().strip()
|
|
489
|
+
return FlagClassifierResult(
|
|
490
|
+
outcome=OUTCOME_MUTATIVE,
|
|
491
|
+
reason=f"awk program contains side-effecting construct: {matched!r}",
|
|
492
|
+
matched_pattern=matched,
|
|
493
|
+
command_family="awk",
|
|
494
|
+
)
|
|
495
|
+
# Found the program text but no mutative pattern
|
|
496
|
+
return FlagClassifierResult(
|
|
497
|
+
outcome=OUTCOME_READ_ONLY,
|
|
498
|
+
reason="awk program does not contain file/system side-effects",
|
|
499
|
+
matched_pattern="awk",
|
|
500
|
+
command_family="awk",
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Could not identify program text -- treat as read-only (conservative)
|
|
504
|
+
return FlagClassifierResult(
|
|
505
|
+
outcome=OUTCOME_READ_ONLY,
|
|
506
|
+
reason="awk with no identifiable program text is read-only",
|
|
507
|
+
matched_pattern="awk",
|
|
508
|
+
command_family="awk",
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
# 11. tar
|
|
513
|
+
def _classify_tar(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
514
|
+
if not tokens or tokens[0] != "tar":
|
|
515
|
+
return None
|
|
516
|
+
args = tokens[1:]
|
|
517
|
+
|
|
518
|
+
# Long-form operation flags
|
|
519
|
+
long_mutative = _has_flag(args, "--create", "--extract", "--append", "--update",
|
|
520
|
+
"--concatenate", "--delete")
|
|
521
|
+
if long_mutative:
|
|
522
|
+
return FlagClassifierResult(
|
|
523
|
+
outcome=OUTCOME_MUTATIVE,
|
|
524
|
+
reason=f"tar {long_mutative} creates or modifies an archive",
|
|
525
|
+
matched_pattern=long_mutative,
|
|
526
|
+
command_family="tar",
|
|
527
|
+
)
|
|
528
|
+
if _has_flag(args, "--list"):
|
|
529
|
+
return FlagClassifierResult(
|
|
530
|
+
outcome=OUTCOME_READ_ONLY,
|
|
531
|
+
reason="tar --list reads archive contents without extracting",
|
|
532
|
+
matched_pattern="--list",
|
|
533
|
+
command_family="tar",
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# Short-form operation letters in bundled flags (e.g. -czvf, -tf, -xvf)
|
|
537
|
+
for a in args:
|
|
538
|
+
if len(a) >= 2 and a[0] == "-" and a[1] != "-":
|
|
539
|
+
letters = a[1:]
|
|
540
|
+
if any(c in letters for c in "cxrua"):
|
|
541
|
+
return FlagClassifierResult(
|
|
542
|
+
outcome=OUTCOME_MUTATIVE,
|
|
543
|
+
reason=f"tar {a} creates or modifies an archive",
|
|
544
|
+
matched_pattern=a,
|
|
545
|
+
command_family="tar",
|
|
546
|
+
)
|
|
547
|
+
if "t" in letters:
|
|
548
|
+
return FlagClassifierResult(
|
|
549
|
+
outcome=OUTCOME_READ_ONLY,
|
|
550
|
+
reason=f"tar {a} lists archive contents without extracting",
|
|
551
|
+
matched_pattern=a,
|
|
552
|
+
command_family="tar",
|
|
553
|
+
)
|
|
554
|
+
# GNU tar also accepts bare operation letters without leading dash
|
|
555
|
+
# as the first non-flag argument (e.g. "tar czf out.tar dir")
|
|
556
|
+
if not a.startswith("-") and len(a) >= 1 and a[0] in "cxtrua":
|
|
557
|
+
if any(c in a for c in "cxrua"):
|
|
558
|
+
return FlagClassifierResult(
|
|
559
|
+
outcome=OUTCOME_MUTATIVE,
|
|
560
|
+
reason=f"tar operation '{a[0]}' creates or modifies an archive",
|
|
561
|
+
matched_pattern=a[0],
|
|
562
|
+
command_family="tar",
|
|
563
|
+
)
|
|
564
|
+
if "t" in a:
|
|
565
|
+
return FlagClassifierResult(
|
|
566
|
+
outcome=OUTCOME_READ_ONLY,
|
|
567
|
+
reason="tar operation 't' lists archive contents",
|
|
568
|
+
matched_pattern="t",
|
|
569
|
+
command_family="tar",
|
|
570
|
+
)
|
|
571
|
+
break
|
|
572
|
+
|
|
573
|
+
# Could not determine operation -- conservative: treat as mutative
|
|
574
|
+
return FlagClassifierResult(
|
|
575
|
+
outcome=OUTCOME_MUTATIVE,
|
|
576
|
+
reason="tar with unrecognized operation flags",
|
|
577
|
+
matched_pattern="tar",
|
|
578
|
+
command_family="tar",
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
# 12. find
|
|
583
|
+
def _classify_find(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
584
|
+
if not tokens or tokens[0] != "find":
|
|
585
|
+
return None
|
|
586
|
+
args = tokens[1:]
|
|
587
|
+
|
|
588
|
+
# Actions that execute external commands or delete files
|
|
589
|
+
mutative_actions = ("-exec", "-execdir", "-delete", "-ok", "-okdir", "-fprint",
|
|
590
|
+
"-fprint0", "-fprintf")
|
|
591
|
+
flag = _has_flag(args, *mutative_actions)
|
|
592
|
+
if flag:
|
|
593
|
+
return FlagClassifierResult(
|
|
594
|
+
outcome=OUTCOME_MUTATIVE,
|
|
595
|
+
reason=f"find {flag} executes commands or modifies the filesystem",
|
|
596
|
+
matched_pattern=flag,
|
|
597
|
+
command_family="find",
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
return FlagClassifierResult(
|
|
601
|
+
outcome=OUTCOME_READ_ONLY,
|
|
602
|
+
reason="find without -exec/-delete is read-only",
|
|
603
|
+
matched_pattern="find",
|
|
604
|
+
command_family="find",
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
# 13. curl
|
|
609
|
+
# HTTP methods that write data to a remote server
|
|
610
|
+
_CURL_WRITE_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
|
|
611
|
+
|
|
612
|
+
def _classify_curl(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
613
|
+
if not tokens or tokens[0] != "curl":
|
|
614
|
+
return None
|
|
615
|
+
args = tokens[1:]
|
|
616
|
+
i = 0
|
|
617
|
+
while i < len(args):
|
|
618
|
+
a = args[i]
|
|
619
|
+
# -X / --request METHOD
|
|
620
|
+
if a in ("-X", "--request"):
|
|
621
|
+
if i + 1 < len(args) and args[i + 1].upper() in _CURL_WRITE_METHODS:
|
|
622
|
+
method = args[i + 1].upper()
|
|
623
|
+
return FlagClassifierResult(
|
|
624
|
+
outcome=OUTCOME_MUTATIVE,
|
|
625
|
+
reason=f"curl -X {method} sends a write request",
|
|
626
|
+
matched_pattern=f"-X {method}",
|
|
627
|
+
command_family="curl",
|
|
628
|
+
)
|
|
629
|
+
i += 2
|
|
630
|
+
continue
|
|
631
|
+
# --request=METHOD
|
|
632
|
+
if a.startswith("--request="):
|
|
633
|
+
method = a.split("=", 1)[1].upper()
|
|
634
|
+
if method in _CURL_WRITE_METHODS:
|
|
635
|
+
return FlagClassifierResult(
|
|
636
|
+
outcome=OUTCOME_MUTATIVE,
|
|
637
|
+
reason=f"curl --request={method} sends a write request",
|
|
638
|
+
matched_pattern=f"--request={method}",
|
|
639
|
+
command_family="curl",
|
|
640
|
+
)
|
|
641
|
+
i += 1
|
|
642
|
+
continue
|
|
643
|
+
# Data flags (imply POST)
|
|
644
|
+
if a in ("-d", "--data", "--data-binary", "--data-raw", "--data-urlencode",
|
|
645
|
+
"-F", "--form", "--form-string",
|
|
646
|
+
"-T", "--upload-file", "--json"):
|
|
647
|
+
return FlagClassifierResult(
|
|
648
|
+
outcome=OUTCOME_MUTATIVE,
|
|
649
|
+
reason=f"curl {a} sends data to the server (implies write)",
|
|
650
|
+
matched_pattern=a,
|
|
651
|
+
command_family="curl",
|
|
652
|
+
)
|
|
653
|
+
i += 1
|
|
654
|
+
|
|
655
|
+
# No write indicators found -- network read
|
|
656
|
+
return FlagClassifierResult(
|
|
657
|
+
outcome=OUTCOME_READ_ONLY,
|
|
658
|
+
reason="curl without write flags performs a network read (GET)",
|
|
659
|
+
matched_pattern="curl",
|
|
660
|
+
command_family="curl",
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
# 14. wget
|
|
665
|
+
_WGET_WRITE_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
|
|
666
|
+
|
|
667
|
+
def _classify_wget(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
668
|
+
if not tokens or tokens[0] != "wget":
|
|
669
|
+
return None
|
|
670
|
+
args = tokens[1:]
|
|
671
|
+
i = 0
|
|
672
|
+
while i < len(args):
|
|
673
|
+
a = args[i]
|
|
674
|
+
# --post-data or --post-file
|
|
675
|
+
if a in ("--post-data", "--post-file"):
|
|
676
|
+
return FlagClassifierResult(
|
|
677
|
+
outcome=OUTCOME_MUTATIVE,
|
|
678
|
+
reason=f"wget {a} sends a POST request",
|
|
679
|
+
matched_pattern=a,
|
|
680
|
+
command_family="wget",
|
|
681
|
+
)
|
|
682
|
+
if a.startswith("--post-data=") or a.startswith("--post-file="):
|
|
683
|
+
return FlagClassifierResult(
|
|
684
|
+
outcome=OUTCOME_MUTATIVE,
|
|
685
|
+
reason=f"wget {a.split('=')[0]} sends a POST request",
|
|
686
|
+
matched_pattern=a.split("=")[0],
|
|
687
|
+
command_family="wget",
|
|
688
|
+
)
|
|
689
|
+
# --method=POST/PUT/DELETE/PATCH
|
|
690
|
+
if a == "--method":
|
|
691
|
+
if i + 1 < len(args) and args[i + 1].upper() in _WGET_WRITE_METHODS:
|
|
692
|
+
method = args[i + 1].upper()
|
|
693
|
+
return FlagClassifierResult(
|
|
694
|
+
outcome=OUTCOME_MUTATIVE,
|
|
695
|
+
reason=f"wget --method {method} sends a write request",
|
|
696
|
+
matched_pattern=f"--method {method}",
|
|
697
|
+
command_family="wget",
|
|
698
|
+
)
|
|
699
|
+
i += 2
|
|
700
|
+
continue
|
|
701
|
+
if a.startswith("--method="):
|
|
702
|
+
method = a.split("=", 1)[1].upper()
|
|
703
|
+
if method in _WGET_WRITE_METHODS:
|
|
704
|
+
return FlagClassifierResult(
|
|
705
|
+
outcome=OUTCOME_MUTATIVE,
|
|
706
|
+
reason=f"wget --method={method} sends a write request",
|
|
707
|
+
matched_pattern=f"--method={method}",
|
|
708
|
+
command_family="wget",
|
|
709
|
+
)
|
|
710
|
+
i += 1
|
|
711
|
+
continue
|
|
712
|
+
# --body-data / --body-file (wget2)
|
|
713
|
+
if a in ("--body-data", "--body-file"):
|
|
714
|
+
return FlagClassifierResult(
|
|
715
|
+
outcome=OUTCOME_MUTATIVE,
|
|
716
|
+
reason=f"wget {a} sends request body data",
|
|
717
|
+
matched_pattern=a,
|
|
718
|
+
command_family="wget",
|
|
719
|
+
)
|
|
720
|
+
if a.startswith("--body-data=") or a.startswith("--body-file="):
|
|
721
|
+
return FlagClassifierResult(
|
|
722
|
+
outcome=OUTCOME_MUTATIVE,
|
|
723
|
+
reason=f"wget {a.split('=')[0]} sends request body data",
|
|
724
|
+
matched_pattern=a.split("=")[0],
|
|
725
|
+
command_family="wget",
|
|
726
|
+
)
|
|
727
|
+
i += 1
|
|
728
|
+
|
|
729
|
+
return FlagClassifierResult(
|
|
730
|
+
outcome=OUTCOME_READ_ONLY,
|
|
731
|
+
reason="wget without write flags performs a network read (GET/download)",
|
|
732
|
+
matched_pattern="wget",
|
|
733
|
+
command_family="wget",
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
# 15. httpie (http / https commands)
|
|
738
|
+
# httpie uses positional method as the first argument: http POST url ...
|
|
739
|
+
# Data items: key=value (string), key:=json (raw JSON), key@file (file)
|
|
740
|
+
_HTTPIE_WRITE_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
|
|
741
|
+
|
|
742
|
+
# Regex to detect httpie data items (key=value, key:=json, key@file)
|
|
743
|
+
_HTTPIE_DATA_ITEM = re.compile(r"^[A-Za-z_][A-Za-z0-9_\-]*(:=|==|=@|:@|@|:=@|=)")
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _classify_httpie(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
747
|
+
if not tokens or tokens[0] not in ("http", "https"):
|
|
748
|
+
return None
|
|
749
|
+
args = tokens[1:]
|
|
750
|
+
|
|
751
|
+
# Skip flags (start with -)
|
|
752
|
+
non_flag_args = [a for a in args if not a.startswith("-")]
|
|
753
|
+
|
|
754
|
+
if not non_flag_args:
|
|
755
|
+
# No non-flag arguments at all -- treat as read-only
|
|
756
|
+
return FlagClassifierResult(
|
|
757
|
+
outcome=OUTCOME_READ_ONLY,
|
|
758
|
+
reason="httpie with no positional arguments is read-only",
|
|
759
|
+
matched_pattern="http",
|
|
760
|
+
command_family="httpie",
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
first = non_flag_args[0].upper()
|
|
764
|
+
# If the first positional arg is an HTTP method
|
|
765
|
+
if first in _HTTPIE_WRITE_METHODS:
|
|
766
|
+
return FlagClassifierResult(
|
|
767
|
+
outcome=OUTCOME_MUTATIVE,
|
|
768
|
+
reason=f"httpie {first} sends a write request",
|
|
769
|
+
matched_pattern=first,
|
|
770
|
+
command_family="httpie",
|
|
771
|
+
)
|
|
772
|
+
# HEAD, GET are read-only explicit methods
|
|
773
|
+
if first in ("GET", "HEAD", "OPTIONS"):
|
|
774
|
+
return FlagClassifierResult(
|
|
775
|
+
outcome=OUTCOME_READ_ONLY,
|
|
776
|
+
reason=f"httpie {first} is a read-only method",
|
|
777
|
+
matched_pattern=first,
|
|
778
|
+
command_family="httpie",
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
# No explicit method: check for data items (imply POST)
|
|
782
|
+
for a in non_flag_args[1:]:
|
|
783
|
+
if _HTTPIE_DATA_ITEM.match(a):
|
|
784
|
+
return FlagClassifierResult(
|
|
785
|
+
outcome=OUTCOME_MUTATIVE,
|
|
786
|
+
reason=f"httpie data item {a!r} implies a POST request",
|
|
787
|
+
matched_pattern=a,
|
|
788
|
+
command_family="httpie",
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
# GET or first arg is URL with no data
|
|
792
|
+
return FlagClassifierResult(
|
|
793
|
+
outcome=OUTCOME_READ_ONLY,
|
|
794
|
+
reason="httpie GET request (no write method or data items)",
|
|
795
|
+
matched_pattern="http",
|
|
796
|
+
command_family="httpie",
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
# ---------------------------------------------------------------------------
|
|
801
|
+
# Registry: maps (base_cmd, optional_subcommand) -> classifier function
|
|
802
|
+
# ---------------------------------------------------------------------------
|
|
803
|
+
# git sub-commands share the "git" base command; they are dispatched via a
|
|
804
|
+
# single "git" entry that delegates to sub-command classifiers.
|
|
805
|
+
|
|
806
|
+
_GIT_SUBCOMMAND_CLASSIFIERS: Dict[str, Callable[[List[str]], Optional[FlagClassifierResult]]] = {
|
|
807
|
+
"push": _classify_git_push,
|
|
808
|
+
"reset": _classify_git_reset,
|
|
809
|
+
"checkout": _classify_git_checkout,
|
|
810
|
+
"stash": _classify_git_stash,
|
|
811
|
+
"rebase": _classify_git_rebase,
|
|
812
|
+
"tag": _classify_git_tag,
|
|
813
|
+
"clean": _classify_git_clean,
|
|
814
|
+
"remote": _classify_git_remote,
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _classify_git_dispatch(tokens: List[str]) -> Optional[FlagClassifierResult]:
|
|
819
|
+
"""Dispatch to the appropriate git sub-command classifier."""
|
|
820
|
+
if len(tokens) < 2 or tokens[0] != "git":
|
|
821
|
+
return None
|
|
822
|
+
sub = tokens[1].lower()
|
|
823
|
+
classifier = _GIT_SUBCOMMAND_CLASSIFIERS.get(sub)
|
|
824
|
+
if classifier is None:
|
|
825
|
+
return None
|
|
826
|
+
return classifier(tokens)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
# Top-level registry: base command -> classifier
|
|
830
|
+
_CLASSIFIER_REGISTRY: Dict[str, Callable[[List[str]], Optional[FlagClassifierResult]]] = {
|
|
831
|
+
"git": _classify_git_dispatch,
|
|
832
|
+
"sed": _classify_sed,
|
|
833
|
+
"awk": _classify_awk,
|
|
834
|
+
"gawk": _classify_awk,
|
|
835
|
+
"mawk": _classify_awk,
|
|
836
|
+
"nawk": _classify_awk,
|
|
837
|
+
"tar": _classify_tar,
|
|
838
|
+
"find": _classify_find,
|
|
839
|
+
"curl": _classify_curl,
|
|
840
|
+
"wget": _classify_wget,
|
|
841
|
+
"http": _classify_httpie,
|
|
842
|
+
"https": _classify_httpie,
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
# ---------------------------------------------------------------------------
|
|
847
|
+
# Public API
|
|
848
|
+
# ---------------------------------------------------------------------------
|
|
849
|
+
|
|
850
|
+
def classify_by_flags(command: str) -> Optional[FlagClassifierResult]:
|
|
851
|
+
"""Classify a command based on flags and sub-commands.
|
|
852
|
+
|
|
853
|
+
This is the primary entry point. Call this BEFORE detect_mutative_command()
|
|
854
|
+
in the classify phase. If this returns a result, it overrides verb-based
|
|
855
|
+
classification. If it returns None, fall through to the existing pipeline.
|
|
856
|
+
|
|
857
|
+
Args:
|
|
858
|
+
command: The full shell command string (already unwrapped if applicable).
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
FlagClassifierResult if the command belongs to a known family, else None.
|
|
862
|
+
"""
|
|
863
|
+
if not command or not command.strip():
|
|
864
|
+
return None
|
|
865
|
+
tokens = _tokenize(command)
|
|
866
|
+
if not tokens:
|
|
867
|
+
return None
|
|
868
|
+
|
|
869
|
+
base_cmd = tokens[0]
|
|
870
|
+
classifier = _CLASSIFIER_REGISTRY.get(base_cmd)
|
|
871
|
+
if classifier is None:
|
|
872
|
+
return None
|
|
873
|
+
return classifier(tokens)
|