@jaguilar87/gaia 5.0.0-rc1
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 +1212 -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 +237 -0
- package/agents/gaia-planner.md +53 -0
- package/agents/gaia-system.md +70 -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 +628 -0
- package/bin/cli/history.py +305 -0
- package/bin/cli/memory.py +464 -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 +816 -0
- package/bin/pre-publish-validate.js +610 -0
- package/bin/python-detect.js +60 -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 +421 -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 +237 -0
- package/dist/gaia-ops/agents/gaia-planner.md +53 -0
- package/dist/gaia-ops/agents/gaia-system.md +70 -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 +421 -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 +163 -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 +232 -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_start.py +81 -0
- package/dist/gaia-ops/hooks/stop_hook.py +82 -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 +154 -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 +182 -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 +82 -0
- package/dist/gaia-ops/skills/gaia-release/reference.md +102 -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/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 +360 -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 +84 -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 +232 -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_start.py +81 -0
- package/dist/gaia-security/hooks/stop_hook.py +82 -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 +232 -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_start.py +81 -0
- package/hooks/stop_hook.py +82 -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 +99 -0
- package/pyproject.toml +32 -0
- package/skills/README.md +154 -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 +182 -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 +82 -0
- package/skills/gaia-release/reference.md +102 -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/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 +360 -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,481 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Network host classification for curl/wget/httpie command targets.
|
|
3
|
+
|
|
4
|
+
This module is a helper consumed by flag_classifiers.py (T3). It is NOT called
|
|
5
|
+
directly from bash_validator.py.
|
|
6
|
+
|
|
7
|
+
Classification categories:
|
|
8
|
+
LOCALHOST -- localhost, 127.0.0.1, ::1, 0.0.0.0, private RFC 1918 ranges
|
|
9
|
+
KNOWN_REGISTRY -- well-known development/CI registries (npm, PyPI, GitHub, etc.)
|
|
10
|
+
UNKNOWN -- all other external targets (triggers T3 approval for GET requests)
|
|
11
|
+
|
|
12
|
+
Private IP ranges (RFC 1918):
|
|
13
|
+
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
|
|
14
|
+
These are treated as LOCALHOST (internal network) for classification purposes.
|
|
15
|
+
|
|
16
|
+
Usage in flag_classifiers (T3):
|
|
17
|
+
- GET + KNOWN_REGISTRY / LOCALHOST => READ_ONLY
|
|
18
|
+
- GET + UNKNOWN => MUTATIVE (T3 approval required)
|
|
19
|
+
- POST/PUT/DELETE/PATCH (any host) => MUTATIVE (always, regardless of host)
|
|
20
|
+
|
|
21
|
+
Dependencies: Python stdlib only (urllib.parse).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
import urllib.parse
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from enum import Enum
|
|
30
|
+
from typing import List, Optional
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Known-safe registries (GET-only; POST to any host is still MUTATIVE)
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# This is a module-level constant. Extend by adding entries to the frozenset.
|
|
37
|
+
|
|
38
|
+
KNOWN_REGISTRIES: frozenset = frozenset({
|
|
39
|
+
# npm
|
|
40
|
+
"registry.npmjs.org",
|
|
41
|
+
"registry.yarnpkg.com",
|
|
42
|
+
"registry.npmmirror.com",
|
|
43
|
+
"www.npmjs.org",
|
|
44
|
+
"npmjs.org",
|
|
45
|
+
# PyPI
|
|
46
|
+
"pypi.org",
|
|
47
|
+
"files.pythonhosted.org",
|
|
48
|
+
# GitHub
|
|
49
|
+
"github.com",
|
|
50
|
+
"api.github.com",
|
|
51
|
+
"raw.githubusercontent.com",
|
|
52
|
+
# Rust / Go / Ruby / PHP
|
|
53
|
+
"crates.io",
|
|
54
|
+
"rubygems.org",
|
|
55
|
+
"packagist.org",
|
|
56
|
+
"pkg.go.dev",
|
|
57
|
+
"proxy.golang.org",
|
|
58
|
+
# Other common CI/build targets
|
|
59
|
+
"dl.google.com",
|
|
60
|
+
"repo.maven.apache.org",
|
|
61
|
+
# Docker registries
|
|
62
|
+
"hub.docker.com",
|
|
63
|
+
"registry.hub.docker.com",
|
|
64
|
+
"ghcr.io",
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
# Localhost addresses (exact match after port stripping)
|
|
68
|
+
_LOCALHOST_EXACT: frozenset = frozenset({
|
|
69
|
+
"localhost",
|
|
70
|
+
"127.0.0.1",
|
|
71
|
+
"0.0.0.0",
|
|
72
|
+
"::1",
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
# RFC 1918 private ranges
|
|
76
|
+
_RFC1918_10 = re.compile(r"^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
|
|
77
|
+
_RFC1918_172 = re.compile(r"^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$")
|
|
78
|
+
_RFC1918_192 = re.compile(r"^192\.168\.\d{1,3}\.\d{1,3}$")
|
|
79
|
+
# Link-local
|
|
80
|
+
_LINK_LOCAL = re.compile(r"^169\.254\.\d{1,3}\.\d{1,3}$")
|
|
81
|
+
# Loopback range beyond 127.0.0.1
|
|
82
|
+
_LOOPBACK_RANGE = re.compile(r"^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
|
|
83
|
+
# *.local mDNS
|
|
84
|
+
_MDNS_LOCAL = re.compile(r"\.local$", re.IGNORECASE)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Result types
|
|
89
|
+
# ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
class HostCategory(str, Enum):
|
|
92
|
+
"""Classification category for a network host."""
|
|
93
|
+
LOCALHOST = "LOCALHOST"
|
|
94
|
+
KNOWN_REGISTRY = "KNOWN_REGISTRY"
|
|
95
|
+
UNKNOWN = "UNKNOWN"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class HostClassification:
|
|
100
|
+
"""Structured result of network host classification.
|
|
101
|
+
|
|
102
|
+
Attributes:
|
|
103
|
+
host: Normalized hostname (no port, no scheme).
|
|
104
|
+
category: One of HostCategory.LOCALHOST, KNOWN_REGISTRY, or UNKNOWN.
|
|
105
|
+
reason: Human-readable explanation.
|
|
106
|
+
"""
|
|
107
|
+
host: str
|
|
108
|
+
category: HostCategory
|
|
109
|
+
reason: str
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def is_local(self) -> bool:
|
|
113
|
+
return self.category == HostCategory.LOCALHOST
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def is_known_registry(self) -> bool:
|
|
117
|
+
return self.category == HostCategory.KNOWN_REGISTRY
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def is_unknown(self) -> bool:
|
|
121
|
+
return self.category == HostCategory.UNKNOWN
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Host extraction helpers
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def _strip_port(host: str) -> str:
|
|
129
|
+
"""Remove port suffix from a bare hostname (host:port -> host)."""
|
|
130
|
+
if host.startswith("["):
|
|
131
|
+
# IPv6 bracketed form: [::1]:8080 -> ::1
|
|
132
|
+
bracket_end = host.find("]")
|
|
133
|
+
if bracket_end != -1:
|
|
134
|
+
return host[1:bracket_end]
|
|
135
|
+
return host
|
|
136
|
+
# Bare IPv6 addresses contain multiple colons (e.g. ::1, fe80::1)
|
|
137
|
+
# Only strip port when there is exactly one colon (host:port form)
|
|
138
|
+
if host.count(":") == 1:
|
|
139
|
+
return host.rsplit(":", 1)[0]
|
|
140
|
+
return host
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _extract_host_from_url(url: str) -> Optional[str]:
|
|
144
|
+
"""Extract the hostname from a URL string.
|
|
145
|
+
|
|
146
|
+
Handles:
|
|
147
|
+
- Full URLs with scheme: https://host:port/path
|
|
148
|
+
- Scheme-relative URLs: //host/path
|
|
149
|
+
- Bare hostname+path: host/path or host:port/path
|
|
150
|
+
- IPv6 literal: http://[::1]:8080/
|
|
151
|
+
|
|
152
|
+
Returns the hostname string without port, or None if not parseable.
|
|
153
|
+
"""
|
|
154
|
+
url = url.strip()
|
|
155
|
+
if not url:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
# URLs with explicit scheme
|
|
159
|
+
if "://" in url:
|
|
160
|
+
try:
|
|
161
|
+
parsed = urllib.parse.urlparse(url)
|
|
162
|
+
if parsed.hostname:
|
|
163
|
+
return parsed.hostname.lower()
|
|
164
|
+
except ValueError:
|
|
165
|
+
pass
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
# Scheme-relative: //host/path
|
|
169
|
+
if url.startswith("//"):
|
|
170
|
+
try:
|
|
171
|
+
parsed = urllib.parse.urlparse("https:" + url)
|
|
172
|
+
if parsed.hostname:
|
|
173
|
+
return parsed.hostname.lower()
|
|
174
|
+
except ValueError:
|
|
175
|
+
pass
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
# Bare form: host/path, host:port, host:port/path
|
|
179
|
+
# Split off any path component
|
|
180
|
+
host_part = url.split("/")[0]
|
|
181
|
+
if not host_part:
|
|
182
|
+
return None
|
|
183
|
+
return _strip_port(host_part).lower()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# ---------------------------------------------------------------------------
|
|
187
|
+
# Primary classification logic
|
|
188
|
+
# ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
def _is_localhost(host: str) -> bool:
|
|
191
|
+
"""Return True when the host is a loopback/local address."""
|
|
192
|
+
if host in _LOCALHOST_EXACT:
|
|
193
|
+
return True
|
|
194
|
+
if _LOOPBACK_RANGE.match(host):
|
|
195
|
+
return True
|
|
196
|
+
if _RFC1918_10.match(host):
|
|
197
|
+
return True
|
|
198
|
+
if _RFC1918_172.match(host):
|
|
199
|
+
return True
|
|
200
|
+
if _RFC1918_192.match(host):
|
|
201
|
+
return True
|
|
202
|
+
if _LINK_LOCAL.match(host):
|
|
203
|
+
return True
|
|
204
|
+
if _MDNS_LOCAL.search(host):
|
|
205
|
+
return True
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def classify_host(url_or_host: str) -> HostClassification:
|
|
210
|
+
"""Classify a URL or bare hostname into LOCALHOST, KNOWN_REGISTRY, or UNKNOWN.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
url_or_host: A full URL (https://registry.npmjs.org/express),
|
|
214
|
+
a bare hostname (registry.npmjs.org),
|
|
215
|
+
or host:port (localhost:8080).
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
HostClassification with category and reason.
|
|
219
|
+
"""
|
|
220
|
+
if not url_or_host or not url_or_host.strip():
|
|
221
|
+
return HostClassification(
|
|
222
|
+
host="",
|
|
223
|
+
category=HostCategory.UNKNOWN,
|
|
224
|
+
reason="empty host/url",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
raw = url_or_host.strip()
|
|
228
|
+
|
|
229
|
+
# Attempt to extract the hostname component
|
|
230
|
+
host = _extract_host_from_url(raw)
|
|
231
|
+
if not host:
|
|
232
|
+
return HostClassification(
|
|
233
|
+
host=raw,
|
|
234
|
+
category=HostCategory.UNKNOWN,
|
|
235
|
+
reason=f"could not parse host from {raw!r}",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# 1. Localhost / loopback / private ranges
|
|
239
|
+
if _is_localhost(host):
|
|
240
|
+
return HostClassification(
|
|
241
|
+
host=host,
|
|
242
|
+
category=HostCategory.LOCALHOST,
|
|
243
|
+
reason=f"localhost/private: {host}",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# 2. Known registries
|
|
247
|
+
if host in KNOWN_REGISTRIES:
|
|
248
|
+
return HostClassification(
|
|
249
|
+
host=host,
|
|
250
|
+
category=HostCategory.KNOWN_REGISTRY,
|
|
251
|
+
reason=f"known registry: {host}",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# 3. Unknown external target
|
|
255
|
+
return HostClassification(
|
|
256
|
+
host=host,
|
|
257
|
+
category=HostCategory.UNKNOWN,
|
|
258
|
+
reason=f"unknown external host: {host}",
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
# URL extraction from command token lists
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
# Flags that consume the NEXT token as their value (not a URL)
|
|
267
|
+
_CURL_VALUE_FLAGS: frozenset = frozenset({
|
|
268
|
+
"-H", "--header",
|
|
269
|
+
"-u", "--user",
|
|
270
|
+
"-A", "--user-agent",
|
|
271
|
+
"-e", "--referer",
|
|
272
|
+
"-o", "--output",
|
|
273
|
+
"-O", "--remote-name",
|
|
274
|
+
"--output-dir",
|
|
275
|
+
"-x", "--proxy",
|
|
276
|
+
"--proxy-user",
|
|
277
|
+
"-T", "--upload-file",
|
|
278
|
+
"-d", "--data",
|
|
279
|
+
"--data-raw", "--data-binary", "--data-urlencode",
|
|
280
|
+
"-F", "--form", "--form-string",
|
|
281
|
+
"-X", "--request",
|
|
282
|
+
"-b", "--cookie",
|
|
283
|
+
"-c", "--cookie-jar",
|
|
284
|
+
"--max-time", "-m",
|
|
285
|
+
"--connect-timeout",
|
|
286
|
+
"-r", "--range",
|
|
287
|
+
"--resolve",
|
|
288
|
+
"--cacert", "--capath",
|
|
289
|
+
"--cert", "--key",
|
|
290
|
+
"--tlspassword",
|
|
291
|
+
"-w", "--write-out",
|
|
292
|
+
"--interface",
|
|
293
|
+
"--dns-servers",
|
|
294
|
+
"--parallel-max",
|
|
295
|
+
"--json",
|
|
296
|
+
"-K", "--config",
|
|
297
|
+
"--limit-rate",
|
|
298
|
+
"--max-redirs",
|
|
299
|
+
"--retry",
|
|
300
|
+
"--retry-delay",
|
|
301
|
+
"--retry-max-time",
|
|
302
|
+
"--socks4", "--socks4a", "--socks5", "--socks5-hostname",
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
_WGET_VALUE_FLAGS: frozenset = frozenset({
|
|
306
|
+
"-O", "--output-document",
|
|
307
|
+
"-o", "--output-file",
|
|
308
|
+
"--append-output",
|
|
309
|
+
"-P", "--directory-prefix",
|
|
310
|
+
"-U", "--user-agent",
|
|
311
|
+
"--referer",
|
|
312
|
+
"--header",
|
|
313
|
+
"--http-user", "--http-password",
|
|
314
|
+
"--proxy-user", "--proxy-password",
|
|
315
|
+
"--post-data", "--post-file",
|
|
316
|
+
"--method",
|
|
317
|
+
"--body-data", "--body-file",
|
|
318
|
+
"--ca-certificate", "--ca-directory",
|
|
319
|
+
"--certificate", "--private-key",
|
|
320
|
+
"--password",
|
|
321
|
+
"-e", "--execute",
|
|
322
|
+
"--bind-address",
|
|
323
|
+
"--connect-timeout",
|
|
324
|
+
"--dns-timeout",
|
|
325
|
+
"--read-timeout",
|
|
326
|
+
"--timeout",
|
|
327
|
+
"--tries", "-t",
|
|
328
|
+
"--waitretry",
|
|
329
|
+
"--wait", "-w",
|
|
330
|
+
"--random-wait",
|
|
331
|
+
"--limit-rate",
|
|
332
|
+
"--quota", "-Q",
|
|
333
|
+
"--input-file", "-i",
|
|
334
|
+
"--base",
|
|
335
|
+
"--domains", "-D",
|
|
336
|
+
"--exclude-domains",
|
|
337
|
+
"--follow-tags",
|
|
338
|
+
"--ignore-tags",
|
|
339
|
+
"--accept", "-A",
|
|
340
|
+
"--reject", "-R",
|
|
341
|
+
"--accept-regex",
|
|
342
|
+
"--reject-regex",
|
|
343
|
+
"--include-directories", "-I",
|
|
344
|
+
"--exclude-directories", "-X",
|
|
345
|
+
"--cut-dirs",
|
|
346
|
+
"--level", "-l",
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def extract_url_from_tokens(tokens: List[str]) -> Optional[str]:
|
|
351
|
+
"""Parse a curl/wget/httpie token list and return the first URL argument.
|
|
352
|
+
|
|
353
|
+
Skips flags and their value arguments to find the bare URL positional arg.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
tokens: Tokenized command list (tokens[0] is the command name).
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
The URL string if found, else None.
|
|
360
|
+
"""
|
|
361
|
+
if not tokens:
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
cmd = tokens[0].lower()
|
|
365
|
+
args = tokens[1:]
|
|
366
|
+
|
|
367
|
+
if cmd == "curl":
|
|
368
|
+
return _extract_url_curl(args)
|
|
369
|
+
if cmd == "wget":
|
|
370
|
+
return _extract_url_wget(args)
|
|
371
|
+
if cmd in ("http", "https"):
|
|
372
|
+
return _extract_url_httpie(args)
|
|
373
|
+
|
|
374
|
+
# Generic fallback: first non-flag argument
|
|
375
|
+
return _extract_url_generic(args)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _extract_url_curl(args: List[str]) -> Optional[str]:
|
|
379
|
+
"""Extract URL from curl arguments, skipping flags and their values."""
|
|
380
|
+
i = 0
|
|
381
|
+
while i < len(args):
|
|
382
|
+
a = args[i]
|
|
383
|
+
# Skip -- end of flags marker
|
|
384
|
+
if a == "--":
|
|
385
|
+
i += 1
|
|
386
|
+
# Everything after -- is treated as positional
|
|
387
|
+
if i < len(args):
|
|
388
|
+
return args[i]
|
|
389
|
+
return None
|
|
390
|
+
# Flags that eat the next token as value
|
|
391
|
+
if a in _CURL_VALUE_FLAGS:
|
|
392
|
+
i += 2
|
|
393
|
+
continue
|
|
394
|
+
# Flags with inline value (--flag=value)
|
|
395
|
+
if a.startswith("--") and "=" in a:
|
|
396
|
+
i += 1
|
|
397
|
+
continue
|
|
398
|
+
# Single-char flags that may or may not eat the next token:
|
|
399
|
+
# if the flag is a single char standalone (e.g. -v, -s, -L, -k) it
|
|
400
|
+
# consumes no value; if it's in our known value set, already handled.
|
|
401
|
+
if a.startswith("-") and len(a) == 2:
|
|
402
|
+
i += 1
|
|
403
|
+
continue
|
|
404
|
+
# Bundled short flags (e.g. -sLk) -- no values consumed; skip
|
|
405
|
+
if a.startswith("-") and len(a) > 2 and a[1] != "-":
|
|
406
|
+
i += 1
|
|
407
|
+
continue
|
|
408
|
+
# Non-flag argument: this is the URL
|
|
409
|
+
if not a.startswith("-"):
|
|
410
|
+
return a
|
|
411
|
+
i += 1
|
|
412
|
+
return None
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _extract_url_wget(args: List[str]) -> Optional[str]:
|
|
416
|
+
"""Extract URL from wget arguments, skipping flags and their values."""
|
|
417
|
+
i = 0
|
|
418
|
+
while i < len(args):
|
|
419
|
+
a = args[i]
|
|
420
|
+
if a == "--":
|
|
421
|
+
i += 1
|
|
422
|
+
if i < len(args):
|
|
423
|
+
return args[i]
|
|
424
|
+
return None
|
|
425
|
+
if a in _WGET_VALUE_FLAGS:
|
|
426
|
+
i += 2
|
|
427
|
+
continue
|
|
428
|
+
if a.startswith("--") and "=" in a:
|
|
429
|
+
i += 1
|
|
430
|
+
continue
|
|
431
|
+
if a.startswith("-") and len(a) == 2:
|
|
432
|
+
i += 1
|
|
433
|
+
continue
|
|
434
|
+
if a.startswith("-") and len(a) > 2 and a[1] != "-":
|
|
435
|
+
i += 1
|
|
436
|
+
continue
|
|
437
|
+
if not a.startswith("-"):
|
|
438
|
+
return a
|
|
439
|
+
i += 1
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
_HTTPIE_METHODS = frozenset({
|
|
444
|
+
"POST", "PUT", "DELETE", "PATCH", "GET", "HEAD", "OPTIONS",
|
|
445
|
+
})
|
|
446
|
+
_HTTPIE_DATA_ITEM = re.compile(r"^[A-Za-z_][A-Za-z0-9_\-]*(:=|==|=@|:@|@|:=@|=)")
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _extract_url_httpie(args: List[str]) -> Optional[str]:
|
|
450
|
+
"""Extract URL from httpie (http/https) arguments.
|
|
451
|
+
|
|
452
|
+
httpie positional syntax: http [METHOD] URL [ITEM ...]
|
|
453
|
+
Skip flags, optional METHOD token, then return the URL.
|
|
454
|
+
"""
|
|
455
|
+
for a in args:
|
|
456
|
+
if a.startswith("-"):
|
|
457
|
+
continue
|
|
458
|
+
# Skip explicit method token
|
|
459
|
+
if a.upper() in _HTTPIE_METHODS:
|
|
460
|
+
continue
|
|
461
|
+
# Skip data items (key=value, key:=json, key@file)
|
|
462
|
+
if _HTTPIE_DATA_ITEM.match(a):
|
|
463
|
+
continue
|
|
464
|
+
# First remaining positional arg is the URL
|
|
465
|
+
return a
|
|
466
|
+
return None
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _extract_url_generic(args: List[str]) -> Optional[str]:
|
|
470
|
+
"""Generic URL extraction: return first non-flag argument."""
|
|
471
|
+
for a in args:
|
|
472
|
+
if a == "--":
|
|
473
|
+
break
|
|
474
|
+
# Long flags with = carry their value inline
|
|
475
|
+
if a.startswith("--") and "=" in a:
|
|
476
|
+
continue
|
|
477
|
+
# Short/long flags without value: skip
|
|
478
|
+
if a.startswith("-"):
|
|
479
|
+
continue
|
|
480
|
+
return a
|
|
481
|
+
return None
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Validate and classify resume prompts for security decision-making.
|
|
2
|
+
|
|
3
|
+
Subsystem 1 of the pre_tool_use Task/Agent path.
|
|
4
|
+
Runs FIRST -- if invalid, nothing else loads.
|
|
5
|
+
|
|
6
|
+
Responsibilities:
|
|
7
|
+
- Validates prompt is not empty, not malformed
|
|
8
|
+
- Detects deprecated approval phrases
|
|
9
|
+
- Detects nonce patterns
|
|
10
|
+
- Returns: classification (nonce/malformed_nonce/deprecated/standard)
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .approval_constants import (
|
|
14
|
+
NONCE_APPROVAL_PREFIX,
|
|
15
|
+
NONCE_APPROVAL_PATTERN,
|
|
16
|
+
DEPRECATED_APPROVAL_PHRASES,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def classify_resume_prompt(prompt: str) -> str:
|
|
21
|
+
"""Classify a resume prompt into one of four categories.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
prompt: The resume prompt string.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
'nonce' -- valid nonce approval token present
|
|
28
|
+
'malformed_nonce' -- APPROVE: prefix present but invalid nonce
|
|
29
|
+
'deprecated' -- deprecated approval phrase detected
|
|
30
|
+
'standard' -- normal resume prompt (no approval indicators)
|
|
31
|
+
"""
|
|
32
|
+
stripped_prompt = prompt.strip()
|
|
33
|
+
if NONCE_APPROVAL_PATTERN.search(prompt):
|
|
34
|
+
return "nonce"
|
|
35
|
+
if stripped_prompt.startswith(NONCE_APPROVAL_PREFIX):
|
|
36
|
+
return "malformed_nonce"
|
|
37
|
+
prompt_lower = prompt.lower()
|
|
38
|
+
if any(phrase in prompt_lower for phrase in DEPRECATED_APPROVAL_PHRASES):
|
|
39
|
+
return "deprecated"
|
|
40
|
+
return "standard"
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shell Unwrapper - Detect and strip wrapper shells from commands.
|
|
3
|
+
|
|
4
|
+
Many commands arrive wrapped in a shell invocation:
|
|
5
|
+
bash -c "actual command"
|
|
6
|
+
sh -c 'rm -rf /tmp/build'
|
|
7
|
+
env bash -c "sh -c 'inner command'"
|
|
8
|
+
|
|
9
|
+
The wrapped inner command is what matters for classification, not the
|
|
10
|
+
wrapper itself. ShellUnwrapper recursively peels wrapper shells until
|
|
11
|
+
it reaches the actual payload.
|
|
12
|
+
|
|
13
|
+
Handles:
|
|
14
|
+
- bash -c, sh -c, zsh -c, dash -c
|
|
15
|
+
- /bin/bash -c, /usr/bin/env bash -c
|
|
16
|
+
- exec bash -c
|
|
17
|
+
- env bash -c (with optional env vars like VAR=val)
|
|
18
|
+
- Nested wrappers: bash -c "sh -c 'inner'"
|
|
19
|
+
- Single-quoted, double-quoted, and unquoted payloads
|
|
20
|
+
|
|
21
|
+
Dependencies: Python stdlib only.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from typing import Optional
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class UnwrapResult:
|
|
33
|
+
"""Result of unwrapping a shell command."""
|
|
34
|
+
|
|
35
|
+
# The innermost command after all wrappers are stripped.
|
|
36
|
+
inner: str
|
|
37
|
+
# Number of wrapper layers removed (0 = no wrapper detected).
|
|
38
|
+
depth: int
|
|
39
|
+
# True if any wrapper was detected and stripped.
|
|
40
|
+
was_wrapped: bool
|
|
41
|
+
|
|
42
|
+
def __str__(self) -> str:
|
|
43
|
+
return self.inner
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Wrapper detection patterns
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Optional path prefix: /bin/, /usr/bin/, /usr/local/bin/
|
|
50
|
+
_OPT_PATH = r"(?:/(?:usr/(?:local/)?)?s?bin/)?"
|
|
51
|
+
|
|
52
|
+
# Optional prefix commands that can precede the shell: env, exec, nohup,
|
|
53
|
+
# sudo, nice, etc. env can carry VAR=val assignments before the shell.
|
|
54
|
+
_PREFIX = (
|
|
55
|
+
r"(?:(?:exec|nohup|sudo|nice|ionice|setsid|time)\s+)*"
|
|
56
|
+
r"(?:(?:" + _OPT_PATH + r")?env\s+(?:[A-Za-z_][A-Za-z_0-9]*=[^\s]*\s+)*)?"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Shell interpreters that accept -c.
|
|
60
|
+
_SHELLS = r"(?:bash|sh|zsh|dash|ksh)"
|
|
61
|
+
|
|
62
|
+
# Combined regex: optional_prefix + optional_path + shell + -c + payload
|
|
63
|
+
# Three capture groups after -c for the three quoting styles:
|
|
64
|
+
# group 1: double-quoted payload
|
|
65
|
+
# group 2: single-quoted payload
|
|
66
|
+
# group 3: unquoted payload (rest of string)
|
|
67
|
+
_WRAPPER_RE = re.compile(
|
|
68
|
+
r"^\s*"
|
|
69
|
+
+ _PREFIX
|
|
70
|
+
+ _OPT_PATH
|
|
71
|
+
+ _SHELLS
|
|
72
|
+
+ r"""\s+-c\s+(?:"((?:[^"\\]|\\.)*)"|'([^']*)'|(\S.*))""",
|
|
73
|
+
re.DOTALL,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Maximum recursion depth to prevent infinite loops on pathological input.
|
|
77
|
+
_MAX_DEPTH = 10
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ShellUnwrapper:
|
|
81
|
+
"""
|
|
82
|
+
Detect and recursively strip shell wrapper invocations.
|
|
83
|
+
|
|
84
|
+
Zero external dependencies -- Python stdlib only.
|
|
85
|
+
|
|
86
|
+
Usage::
|
|
87
|
+
|
|
88
|
+
unwrapper = ShellUnwrapper()
|
|
89
|
+
result = unwrapper.unwrap('bash -c "ls -la"')
|
|
90
|
+
# result.inner == "ls -la"
|
|
91
|
+
# result.depth == 1
|
|
92
|
+
# result.was_wrapped == True
|
|
93
|
+
|
|
94
|
+
result = unwrapper.unwrap("ls -la")
|
|
95
|
+
# result.inner == "ls -la"
|
|
96
|
+
# result.depth == 0
|
|
97
|
+
# result.was_wrapped == False
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def unwrap(self, command: str) -> UnwrapResult:
|
|
101
|
+
"""
|
|
102
|
+
Recursively strip shell wrappers from *command*.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
command: Raw shell command string.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
UnwrapResult with the innermost command, depth, and whether
|
|
109
|
+
any wrapper was detected.
|
|
110
|
+
"""
|
|
111
|
+
if not command or not command.strip():
|
|
112
|
+
return UnwrapResult(inner=command or "", depth=0, was_wrapped=False)
|
|
113
|
+
|
|
114
|
+
current = command.strip()
|
|
115
|
+
depth = 0
|
|
116
|
+
|
|
117
|
+
while depth < _MAX_DEPTH:
|
|
118
|
+
inner = self._try_unwrap_once(current)
|
|
119
|
+
if inner is None:
|
|
120
|
+
break
|
|
121
|
+
current = inner.strip()
|
|
122
|
+
depth += 1
|
|
123
|
+
|
|
124
|
+
return UnwrapResult(
|
|
125
|
+
inner=current,
|
|
126
|
+
depth=depth,
|
|
127
|
+
was_wrapped=depth > 0,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def is_wrapped(self, command: str) -> bool:
|
|
131
|
+
"""Return True if *command* has a shell wrapper layer."""
|
|
132
|
+
if not command or not command.strip():
|
|
133
|
+
return False
|
|
134
|
+
return self._try_unwrap_once(command.strip()) is not None
|
|
135
|
+
|
|
136
|
+
# ------------------------------------------------------------------
|
|
137
|
+
# Internal
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def _try_unwrap_once(self, command: str) -> Optional[str]:
|
|
141
|
+
"""
|
|
142
|
+
Attempt to strip one wrapper layer.
|
|
143
|
+
|
|
144
|
+
Returns the inner payload string, or None if no wrapper detected.
|
|
145
|
+
"""
|
|
146
|
+
m = _WRAPPER_RE.match(command)
|
|
147
|
+
if m is None:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
# Exactly one of the three groups will be non-None.
|
|
151
|
+
payload = m.group(1) # double-quoted
|
|
152
|
+
if payload is not None:
|
|
153
|
+
# Unescape \" and \\ inside double-quoted payload
|
|
154
|
+
payload = payload.replace('\\"', '"').replace("\\\\", "\\")
|
|
155
|
+
return payload.strip()
|
|
156
|
+
|
|
157
|
+
payload = m.group(2) # single-quoted
|
|
158
|
+
if payload is not None:
|
|
159
|
+
return payload.strip()
|
|
160
|
+
|
|
161
|
+
payload = m.group(3) # unquoted
|
|
162
|
+
if payload is not None:
|
|
163
|
+
return payload.strip()
|
|
164
|
+
|
|
165
|
+
return None
|