code-muse 0.0.1__py3-none-any.whl
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.
- code_muse/__init__.py +26 -0
- code_muse/__main__.py +10 -0
- code_muse/agents/__init__.py +31 -0
- code_muse/agents/_builder.py +214 -0
- code_muse/agents/_compaction.py +506 -0
- code_muse/agents/_diagnostics.py +171 -0
- code_muse/agents/_history.py +382 -0
- code_muse/agents/_key_listeners.py +148 -0
- code_muse/agents/_non_streaming_render.py +148 -0
- code_muse/agents/_runtime.py +596 -0
- code_muse/agents/agent_creator_agent.py +603 -0
- code_muse/agents/agent_helios.py +47 -0
- code_muse/agents/agent_manager.py +740 -0
- code_muse/agents/agent_muse.py +78 -0
- code_muse/agents/agent_planning.py +44 -0
- code_muse/agents/agent_qa_melpomene.py +207 -0
- code_muse/agents/base_agent.py +194 -0
- code_muse/agents/event_stream_handler.py +361 -0
- code_muse/agents/json_agent.py +201 -0
- code_muse/agents/prompt_v3.py +521 -0
- code_muse/agents/subagent_stream_handler.py +273 -0
- code_muse/callbacks.py +941 -0
- code_muse/chatgpt_codex_client.py +333 -0
- code_muse/claude_cache_client.py +853 -0
- code_muse/cli_runner/__init__.py +319 -0
- code_muse/cli_runner/args.py +63 -0
- code_muse/cli_runner/loop.py +510 -0
- code_muse/cli_runner/resume.py +72 -0
- code_muse/cli_runner/runner.py +161 -0
- code_muse/command_line/__init__.py +1 -0
- code_muse/command_line/add_model_menu.py +1331 -0
- code_muse/command_line/agent_menu.py +674 -0
- code_muse/command_line/attachments.py +397 -0
- code_muse/command_line/autosave_menu.py +709 -0
- code_muse/command_line/clipboard.py +528 -0
- code_muse/command_line/colors_menu.py +530 -0
- code_muse/command_line/command_handler.py +262 -0
- code_muse/command_line/command_registry.py +150 -0
- code_muse/command_line/config_commands.py +711 -0
- code_muse/command_line/core_commands.py +740 -0
- code_muse/command_line/diff_menu.py +865 -0
- code_muse/command_line/file_path_completion.py +73 -0
- code_muse/command_line/load_context_completion.py +57 -0
- code_muse/command_line/model_picker_completion.py +512 -0
- code_muse/command_line/model_settings_menu.py +983 -0
- code_muse/command_line/onboarding_slides.py +162 -0
- code_muse/command_line/onboarding_wizard.py +337 -0
- code_muse/command_line/pagination.py +41 -0
- code_muse/command_line/pin_command_completion.py +329 -0
- code_muse/command_line/prompt_toolkit_completion.py +886 -0
- code_muse/command_line/session_commands.py +304 -0
- code_muse/command_line/shell_passthrough.py +145 -0
- code_muse/command_line/skills_completion.py +158 -0
- code_muse/command_line/types.py +18 -0
- code_muse/command_line/uc_menu.py +908 -0
- code_muse/command_line/utils.py +105 -0
- code_muse/command_line/wiggum_state.py +77 -0
- code_muse/config.py +1138 -0
- code_muse/config_agent.py +168 -0
- code_muse/config_appearance.py +241 -0
- code_muse/config_model.py +357 -0
- code_muse/config_security.py +73 -0
- code_muse/error_logging.py +132 -0
- code_muse/evals/__init__.py +35 -0
- code_muse/evals/eval_helpers.py +81 -0
- code_muse/evals/eval_runner.py +299 -0
- code_muse/evals/sample_evals/__init__.py +1 -0
- code_muse/evals/sample_evals/eval_frugal_reads.py +59 -0
- code_muse/evals/sample_evals/eval_memory_planning.py +31 -0
- code_muse/evals/sample_evals/eval_shell_efficiency.py +39 -0
- code_muse/evals/sample_evals/eval_tool_masking.py +33 -0
- code_muse/fs_scan_cache/__init__.py +31 -0
- code_muse/fs_scan_cache/invalidation_hooks.py +89 -0
- code_muse/fs_scan_cache/scan_cache_core.cpython-314-darwin.so +0 -0
- code_muse/fs_scan_cache/scan_cache_core.pyx +203 -0
- code_muse/fs_scan_cache/tool_integration.py +309 -0
- code_muse/fs_scan_cache/ttl_policy.py +44 -0
- code_muse/gemini_code_assist.py +383 -0
- code_muse/gemini_model.py +838 -0
- code_muse/hook_engine/README.md +105 -0
- code_muse/hook_engine/__init__.py +21 -0
- code_muse/hook_engine/aliases.py +153 -0
- code_muse/hook_engine/engine.py +221 -0
- code_muse/hook_engine/executor.py +347 -0
- code_muse/hook_engine/matcher.py +154 -0
- code_muse/hook_engine/models.py +245 -0
- code_muse/hook_engine/registry.py +114 -0
- code_muse/hook_engine/trust.py +268 -0
- code_muse/hook_engine/validator.py +144 -0
- code_muse/http_utils.py +360 -0
- code_muse/keymap.py +128 -0
- code_muse/list_filtering.py +26 -0
- code_muse/main.py +10 -0
- code_muse/messaging/__init__.py +259 -0
- code_muse/messaging/bus.py +621 -0
- code_muse/messaging/commands.py +166 -0
- code_muse/messaging/markdown_patches.py +57 -0
- code_muse/messaging/message_queue.py +397 -0
- code_muse/messaging/messages.py +591 -0
- code_muse/messaging/queue_console.py +269 -0
- code_muse/messaging/renderers.py +308 -0
- code_muse/messaging/rich_renderer.py +1158 -0
- code_muse/messaging/shimmer.py +154 -0
- code_muse/messaging/spinner/__init__.py +87 -0
- code_muse/messaging/spinner/console_spinner.py +250 -0
- code_muse/messaging/spinner/spinner_base.py +82 -0
- code_muse/messaging/subagent_console.py +458 -0
- code_muse/model_factory.py +1203 -0
- code_muse/model_switching.py +59 -0
- code_muse/model_utils.py +156 -0
- code_muse/models.json +66 -0
- code_muse/models_cache/__init__.py +26 -0
- code_muse/models_cache/blocking_lru_cache.py +98 -0
- code_muse/models_cache/cache_writer.py +86 -0
- code_muse/models_cache/sha256_hash.cpython-314-darwin.so +0 -0
- code_muse/models_cache/sha256_hash.pyx +34 -0
- code_muse/models_cache/startup_integration.py +75 -0
- code_muse/models_dev_api.json +1 -0
- code_muse/models_dev_parser.py +590 -0
- code_muse/motion.py +126 -0
- code_muse/plugins/__init__.py +471 -0
- code_muse/plugins/agent_skills/__init__.py +32 -0
- code_muse/plugins/agent_skills/config.py +176 -0
- code_muse/plugins/agent_skills/discovery.py +309 -0
- code_muse/plugins/agent_skills/downloader.py +389 -0
- code_muse/plugins/agent_skills/installer.py +19 -0
- code_muse/plugins/agent_skills/metadata.py +293 -0
- code_muse/plugins/agent_skills/prompt_builder.py +66 -0
- code_muse/plugins/agent_skills/register_callbacks.py +298 -0
- code_muse/plugins/agent_skills/remote_catalog.py +320 -0
- code_muse/plugins/agent_skills/skill_catalog.py +254 -0
- code_muse/plugins/agent_skills/skills_install_menu.py +690 -0
- code_muse/plugins/agent_skills/skills_menu.py +791 -0
- code_muse/plugins/autonomous_memory/__init__.py +39 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-darwin.so +0 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/autonomous_memory/bm25_scorer.pyx +291 -0
- code_muse/plugins/autonomous_memory/consolidation.py +82 -0
- code_muse/plugins/autonomous_memory/extraction.py +382 -0
- code_muse/plugins/autonomous_memory/lease_lock.py +105 -0
- code_muse/plugins/autonomous_memory/memory_injection.py +59 -0
- code_muse/plugins/autonomous_memory/register_callbacks.py +268 -0
- code_muse/plugins/autonomous_memory/secret_scanner.py +62 -0
- code_muse/plugins/autonomous_memory/session_scanner.py +163 -0
- code_muse/plugins/aws_bedrock/__init__.py +14 -0
- code_muse/plugins/aws_bedrock/config.py +99 -0
- code_muse/plugins/aws_bedrock/register_callbacks.py +241 -0
- code_muse/plugins/aws_bedrock/utils.py +153 -0
- code_muse/plugins/azure_foundry/README.md +238 -0
- code_muse/plugins/azure_foundry/__init__.py +15 -0
- code_muse/plugins/azure_foundry/config.py +125 -0
- code_muse/plugins/azure_foundry/discovery.py +187 -0
- code_muse/plugins/azure_foundry/register_callbacks.py +495 -0
- code_muse/plugins/azure_foundry/token.py +180 -0
- code_muse/plugins/azure_foundry/utils.py +345 -0
- code_muse/plugins/build_filter/__init__.py +1 -0
- code_muse/plugins/build_filter/register_callbacks.py +201 -0
- code_muse/plugins/build_filter/strategies/__init__.py +1 -0
- code_muse/plugins/build_filter/strategies/build.py +397 -0
- code_muse/plugins/chatgpt_oauth/__init__.py +6 -0
- code_muse/plugins/chatgpt_oauth/config.py +52 -0
- code_muse/plugins/chatgpt_oauth/oauth_flow.py +338 -0
- code_muse/plugins/chatgpt_oauth/register_callbacks.py +172 -0
- code_muse/plugins/chatgpt_oauth/test_plugin.py +301 -0
- code_muse/plugins/chatgpt_oauth/utils.py +538 -0
- code_muse/plugins/checkpointing/__init__.py +29 -0
- code_muse/plugins/checkpointing/checkpoint_hook.py +51 -0
- code_muse/plugins/checkpointing/conversation_snapshots.py +117 -0
- code_muse/plugins/checkpointing/register_callbacks.py +51 -0
- code_muse/plugins/checkpointing/restore_command.py +263 -0
- code_muse/plugins/checkpointing/rewind_shortcut.py +88 -0
- code_muse/plugins/checkpointing/shadow_git.py +90 -0
- code_muse/plugins/claude_code_hooks/__init__.py +1 -0
- code_muse/plugins/claude_code_hooks/config.py +188 -0
- code_muse/plugins/claude_code_hooks/register_callbacks.py +208 -0
- code_muse/plugins/claude_code_oauth/README.md +167 -0
- code_muse/plugins/claude_code_oauth/SETUP.md +93 -0
- code_muse/plugins/claude_code_oauth/__init__.py +25 -0
- code_muse/plugins/claude_code_oauth/config.py +52 -0
- code_muse/plugins/claude_code_oauth/fast_mode.py +124 -0
- code_muse/plugins/claude_code_oauth/prompt_handler.py +63 -0
- code_muse/plugins/claude_code_oauth/register_callbacks.py +547 -0
- code_muse/plugins/claude_code_oauth/test_fast_mode.py +165 -0
- code_muse/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_muse/plugins/claude_code_oauth/token_refresh_heartbeat.py +237 -0
- code_muse/plugins/claude_code_oauth/utils.py +664 -0
- code_muse/plugins/copilot_auth/__init__.py +11 -0
- code_muse/plugins/copilot_auth/config.py +91 -0
- code_muse/plugins/copilot_auth/reasoning_client.py +409 -0
- code_muse/plugins/copilot_auth/register_callbacks.py +461 -0
- code_muse/plugins/copilot_auth/utils.py +584 -0
- code_muse/plugins/custom_commands/__init__.py +14 -0
- code_muse/plugins/custom_commands/args_injection.py +82 -0
- code_muse/plugins/custom_commands/command_discovery.py +89 -0
- code_muse/plugins/custom_commands/command_toml_schema.py +71 -0
- code_muse/plugins/custom_commands/register_callbacks.py +176 -0
- code_muse/plugins/customizable_commands/__init__.py +0 -0
- code_muse/plugins/customizable_commands/register_callbacks.py +136 -0
- code_muse/plugins/destructive_command_guard/__init__.py +14 -0
- code_muse/plugins/destructive_command_guard/detector.py +375 -0
- code_muse/plugins/destructive_command_guard/register_callbacks.py +148 -0
- code_muse/plugins/example_custom_command/README.md +280 -0
- code_muse/plugins/example_custom_command/register_callbacks.py +51 -0
- code_muse/plugins/file_permission_handler/__init__.py +4 -0
- code_muse/plugins/file_permission_handler/register_callbacks.py +441 -0
- code_muse/plugins/filter_engine/__init__.py +30 -0
- code_muse/plugins/filter_engine/classifier.py +153 -0
- code_muse/plugins/filter_engine/content_detector.py +184 -0
- code_muse/plugins/filter_engine/dispatcher.py +244 -0
- code_muse/plugins/filter_engine/register_callbacks.py +188 -0
- code_muse/plugins/filter_engine/registry.py +279 -0
- code_muse/plugins/filter_engine/strategies/__init__.py +8 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/ast_compressor.pyx +348 -0
- code_muse/plugins/filter_engine/strategies/ast_parser.py +167 -0
- code_muse/plugins/filter_engine/strategies/code.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/code.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/code.pyx +584 -0
- code_muse/plugins/filter_engine/strategies/git.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/git.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/git.pyx +438 -0
- code_muse/plugins/filter_engine/strategies/json_compressor.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/json_compressor.pyx +253 -0
- code_muse/plugins/filter_engine/strategies/json_patterns.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/json_patterns.pyx +178 -0
- code_muse/plugins/filter_engine/strategies/lint.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/lint.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/lint.pyx +626 -0
- code_muse/plugins/filter_engine/strategies/test.cpython-314-darwin.so +0 -0
- code_muse/plugins/filter_engine/strategies/test.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/plugins/filter_engine/strategies/test.pyx +431 -0
- code_muse/plugins/filter_engine/verbosity.py +63 -0
- code_muse/plugins/force_push_guard/__init__.py +5 -0
- code_muse/plugins/force_push_guard/detector.py +96 -0
- code_muse/plugins/force_push_guard/register_callbacks.py +144 -0
- code_muse/plugins/force_push_guard/test_detector.py +143 -0
- code_muse/plugins/frontend_emitter/__init__.py +25 -0
- code_muse/plugins/frontend_emitter/emitter.py +121 -0
- code_muse/plugins/frontend_emitter/register_callbacks.py +259 -0
- code_muse/plugins/gac/__init__.py +4 -0
- code_muse/plugins/gac/git_ops.py +136 -0
- code_muse/plugins/gac/prompt.py +191 -0
- code_muse/plugins/gac/register_callbacks.py +82 -0
- code_muse/plugins/hook_creator/__init__.py +1 -0
- code_muse/plugins/hook_creator/register_callbacks.py +34 -0
- code_muse/plugins/hook_manager/__init__.py +1 -0
- code_muse/plugins/hook_manager/config.py +289 -0
- code_muse/plugins/hook_manager/hooks_menu.py +563 -0
- code_muse/plugins/hook_manager/register_callbacks.py +227 -0
- code_muse/plugins/hook_monitor/register_callbacks.py +36 -0
- code_muse/plugins/mindpack/__init__.py +0 -0
- code_muse/plugins/mindpack/factory.py +930 -0
- code_muse/plugins/mindpack/judge.py +573 -0
- code_muse/plugins/mindpack/memory.py +100 -0
- code_muse/plugins/mindpack/mindpack_menu.py +1552 -0
- code_muse/plugins/mindpack/orchestration.py +605 -0
- code_muse/plugins/mindpack/register_callbacks.py +175 -0
- code_muse/plugins/mindpack/schemas.py +358 -0
- code_muse/plugins/mindpack/tools.py +387 -0
- code_muse/plugins/oauth_muse_html.py +226 -0
- code_muse/plugins/ollama_setup/__init__.py +5 -0
- code_muse/plugins/ollama_setup/completer.py +36 -0
- code_muse/plugins/ollama_setup/register_callbacks.py +410 -0
- code_muse/plugins/plan_command/__init__.py +0 -0
- code_muse/plugins/plan_command/register_callbacks.py +206 -0
- code_muse/plugins/plan_mode/__init__.py +37 -0
- code_muse/plugins/plan_mode/mode_cycling.py +40 -0
- code_muse/plugins/plan_mode/plan_generation.py +68 -0
- code_muse/plugins/plan_mode/plan_hooks.py +74 -0
- code_muse/plugins/plan_mode/plan_mode_tools.py +138 -0
- code_muse/plugins/plan_mode/register_callbacks.py +121 -0
- code_muse/plugins/plugin_trust/register_callbacks.py +140 -0
- code_muse/plugins/policy_engine/__init__.py +46 -0
- code_muse/plugins/policy_engine/approval_flow_integration.py +59 -0
- code_muse/plugins/policy_engine/policy_evaluator.py +75 -0
- code_muse/plugins/policy_engine/policy_file_discovery.py +90 -0
- code_muse/plugins/policy_engine/policy_toml_schema.py +115 -0
- code_muse/plugins/policy_engine/register_callbacks.py +112 -0
- code_muse/plugins/pop_command/__init__.py +1 -0
- code_muse/plugins/pop_command/register_callbacks.py +189 -0
- code_muse/plugins/prompt_newline/__init__.py +13 -0
- code_muse/plugins/prompt_newline/config.py +19 -0
- code_muse/plugins/prompt_newline/register_callbacks.py +159 -0
- code_muse/plugins/safety_status/__init__.py +0 -0
- code_muse/plugins/safety_status/register_callbacks.py +113 -0
- code_muse/plugins/semantic_compression/__init__.py +6 -0
- code_muse/plugins/semantic_compression/compressor.py +295 -0
- code_muse/plugins/semantic_compression/config.py +123 -0
- code_muse/plugins/semantic_compression/register_callbacks.py +320 -0
- code_muse/plugins/shell_minimizer/__init__.py +50 -0
- code_muse/plugins/shell_minimizer/builtin_filters.toml +393 -0
- code_muse/plugins/shell_minimizer/pipeline.py +556 -0
- code_muse/plugins/shell_minimizer/primitives.py +482 -0
- code_muse/plugins/shell_minimizer/register_callbacks.py +276 -0
- code_muse/plugins/shell_safety/__init__.py +6 -0
- code_muse/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_muse/plugins/shell_safety/command_cache.py +149 -0
- code_muse/plugins/shell_safety/register_callbacks.py +202 -0
- code_muse/plugins/synthetic_status/__init__.py +1 -0
- code_muse/plugins/synthetic_status/register_callbacks.py +128 -0
- code_muse/plugins/synthetic_status/status_api.py +145 -0
- code_muse/plugins/token_caching/__init__.py +21 -0
- code_muse/plugins/token_caching/cache_hit_tracking.py +128 -0
- code_muse/plugins/token_caching/cacheable_prefix_detection.py +28 -0
- code_muse/plugins/token_caching/register_callbacks.py +54 -0
- code_muse/plugins/token_caching/stats_display.py +35 -0
- code_muse/plugins/token_tracking/__init__.py +26 -0
- code_muse/plugins/token_tracking/database.py +381 -0
- code_muse/plugins/token_tracking/edit_analyzer.py +97 -0
- code_muse/plugins/token_tracking/record.py +55 -0
- code_muse/plugins/token_tracking/register_callbacks.py +277 -0
- code_muse/plugins/token_tracking/reports.py +329 -0
- code_muse/plugins/universal_constructor/__init__.py +13 -0
- code_muse/plugins/universal_constructor/models.py +136 -0
- code_muse/plugins/universal_constructor/register_callbacks.py +47 -0
- code_muse/plugins/universal_constructor/registry.py +390 -0
- code_muse/plugins/universal_constructor/runner.py +474 -0
- code_muse/plugins/universal_constructor/safety.py +440 -0
- code_muse/plugins/universal_constructor/sandbox.py +584 -0
- code_muse/provider_identity.py +105 -0
- code_muse/pydantic_patches.py +410 -0
- code_muse/reopenable_async_client.py +233 -0
- code_muse/round_robin_model.py +151 -0
- code_muse/secret_storage.py +74 -0
- code_muse/security/__init__.py +1 -0
- code_muse/security/redaction.cpython-314-darwin.so +0 -0
- code_muse/security/redaction.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/security/redaction.pyx +135 -0
- code_muse/session_storage.py +565 -0
- code_muse/status_display.py +261 -0
- code_muse/stream_parser/__init__.py +76 -0
- code_muse/stream_parser/assistant_text_parser.py +90 -0
- code_muse/stream_parser/citation_parser.py +76 -0
- code_muse/stream_parser/inline_hidden_tag_parser.py +236 -0
- code_muse/stream_parser/proposed_plan_parser.py +158 -0
- code_muse/stream_parser/stream_text_chunk.py +23 -0
- code_muse/stream_parser/stream_text_parser.py +27 -0
- code_muse/stream_parser/tagged_line_parser.cpython-314-darwin.so +0 -0
- code_muse/stream_parser/tagged_line_parser.pyx +251 -0
- code_muse/stream_parser/utf8_stream_parser.cpython-314-darwin.so +0 -0
- code_muse/stream_parser/utf8_stream_parser.pyx +206 -0
- code_muse/summarization_agent.py +308 -0
- code_muse/terminal_utils.cpython-314-darwin.so +0 -0
- code_muse/terminal_utils.cpython-314-x86_64-linux-gnu.so +0 -0
- code_muse/terminal_utils.pyx +483 -0
- code_muse/tools/__init__.py +459 -0
- code_muse/tools/agent_tools.py +613 -0
- code_muse/tools/ask_user_question/__init__.py +26 -0
- code_muse/tools/ask_user_question/constants.py +73 -0
- code_muse/tools/ask_user_question/demo_tui.py +55 -0
- code_muse/tools/ask_user_question/handler.py +232 -0
- code_muse/tools/ask_user_question/models.py +302 -0
- code_muse/tools/ask_user_question/registration.py +37 -0
- code_muse/tools/ask_user_question/renderers.py +336 -0
- code_muse/tools/ask_user_question/terminal_ui.py +327 -0
- code_muse/tools/ask_user_question/theme.py +156 -0
- code_muse/tools/ask_user_question/tui_loop.py +422 -0
- code_muse/tools/background_jobs.py +99 -0
- code_muse/tools/browser/__init__.py +37 -0
- code_muse/tools/browser/browser_control.py +289 -0
- code_muse/tools/browser/browser_interactions.py +545 -0
- code_muse/tools/browser/browser_locators.py +640 -0
- code_muse/tools/browser/browser_manager.py +376 -0
- code_muse/tools/browser/browser_navigation.py +251 -0
- code_muse/tools/browser/browser_screenshot.py +180 -0
- code_muse/tools/browser/browser_scripts.py +462 -0
- code_muse/tools/browser/browser_workflows.py +222 -0
- code_muse/tools/chrome_cdp/__init__.py +1070 -0
- code_muse/tools/chrome_cdp/register_callbacks.py +61 -0
- code_muse/tools/command_runner.py +1401 -0
- code_muse/tools/common.py +1407 -0
- code_muse/tools/display.py +87 -0
- code_muse/tools/file_modifications.py +1099 -0
- code_muse/tools/file_operations.py +860 -0
- code_muse/tools/image_tools.py +185 -0
- code_muse/tools/meetin_proxy/__init__.py +243 -0
- code_muse/tools/meetin_proxy/capture_addon.py +82 -0
- code_muse/tools/meetin_proxy/proxy_manager.py +326 -0
- code_muse/tools/meetin_proxy/register_callbacks.py +45 -0
- code_muse/tools/path_policy.py +219 -0
- code_muse/tools/skills_tools.py +586 -0
- code_muse/tools/subagent_context.py +158 -0
- code_muse/tools/tools_content.py +50 -0
- code_muse/tools/universal_constructor.py +965 -0
- code_muse/uvx_detection.py +241 -0
- code_muse/version_checker.py +86 -0
- code_muse-0.0.1.data/data/code_muse/models.json +66 -0
- code_muse-0.0.1.data/data/code_muse/models_dev_api.json +1 -0
- code_muse-0.0.1.dist-info/METADATA +845 -0
- code_muse-0.0.1.dist-info/RECORD +394 -0
- code_muse-0.0.1.dist-info/WHEEL +4 -0
- code_muse-0.0.1.dist-info/entry_points.txt +2 -0
- code_muse-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Manage mitmdump subprocess for traffic capture."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import shutil
|
|
8
|
+
import signal
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import time
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from code_muse.http_utils import find_available_port
|
|
16
|
+
from code_muse.messaging import emit_info, emit_success, emit_warning
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Global singleton manager instance
|
|
21
|
+
_manager: MitmProxyManager | None = None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_manager() -> MitmProxyManager:
|
|
25
|
+
global _manager
|
|
26
|
+
if _manager is None:
|
|
27
|
+
_manager = MitmProxyManager()
|
|
28
|
+
return _manager
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MitmProxyManager:
|
|
32
|
+
"""Lifecycle manager for mitmdump capture process."""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self._process: subprocess.Popen | None = None
|
|
36
|
+
self._port: int | None = None
|
|
37
|
+
self._target: str = ""
|
|
38
|
+
self._output_path: str = ""
|
|
39
|
+
self._addon_path: str = ""
|
|
40
|
+
self._flow_count: int = 0
|
|
41
|
+
self._saved_proxy_env: dict[str, str | None] = {}
|
|
42
|
+
|
|
43
|
+
def find_mitmdump(self) -> str | None:
|
|
44
|
+
"""Locate mitmdump binary across common locations."""
|
|
45
|
+
# 1. PATH
|
|
46
|
+
path_mitmdump = shutil.which("mitmdump")
|
|
47
|
+
if path_mitmdump:
|
|
48
|
+
return path_mitmdump
|
|
49
|
+
|
|
50
|
+
# 2. Local mitmproxy checkout (uv run)
|
|
51
|
+
local_checkout = Path("/Users/adam2/projects/mitmproxy")
|
|
52
|
+
if (local_checkout / "pyproject.toml").exists():
|
|
53
|
+
# Prefer uv run if uv is available
|
|
54
|
+
if shutil.which("uv"):
|
|
55
|
+
return f"cd {local_checkout} && uv run mitmdump"
|
|
56
|
+
# Fallback: direct python module invocation
|
|
57
|
+
python = sys.executable
|
|
58
|
+
return f"{python} -m mitmproxy.tools.main mitmdump"
|
|
59
|
+
|
|
60
|
+
# 3. pip location — mitmdump is usually on PATH, but check module invocation
|
|
61
|
+
try:
|
|
62
|
+
import mitmproxy
|
|
63
|
+
|
|
64
|
+
return f"{sys.executable} -m mitmproxy.tools.main mitmdump"
|
|
65
|
+
except ImportError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def _resolve_addon_path(self) -> str:
|
|
71
|
+
"""Resolve path to the capture addon script."""
|
|
72
|
+
if self._addon_path and Path(self._addon_path).exists():
|
|
73
|
+
return self._addon_path
|
|
74
|
+
|
|
75
|
+
# Relative to this module
|
|
76
|
+
module_dir = Path(__file__).parent
|
|
77
|
+
addon = module_dir / "capture_addon.py"
|
|
78
|
+
if addon.exists():
|
|
79
|
+
return str(addon)
|
|
80
|
+
|
|
81
|
+
# Package data fallback
|
|
82
|
+
try:
|
|
83
|
+
import importlib.resources as resources
|
|
84
|
+
|
|
85
|
+
with resources.path(
|
|
86
|
+
"code_muse.tools.meetin_proxy", "capture_addon.py"
|
|
87
|
+
) as p:
|
|
88
|
+
return str(p)
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
raise FileNotFoundError("Could not resolve capture_addon.py path")
|
|
93
|
+
|
|
94
|
+
def start(
|
|
95
|
+
self,
|
|
96
|
+
target: str = "",
|
|
97
|
+
port: int | None = None,
|
|
98
|
+
max_req_body: int = 10000,
|
|
99
|
+
max_res_body: int = 100000,
|
|
100
|
+
) -> int:
|
|
101
|
+
"""Start mitmdump with capture addon.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
target: Domain filter (empty captures everything).
|
|
105
|
+
port: Listen port (auto-selected if None).
|
|
106
|
+
max_req_body: Max request body bytes to capture.
|
|
107
|
+
max_res_body: Max response body bytes to capture.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The listening port number.
|
|
111
|
+
"""
|
|
112
|
+
if self._process is not None and self._process.poll() is None:
|
|
113
|
+
emit_warning("mitmproxy already running — stopping previous instance first")
|
|
114
|
+
self.stop()
|
|
115
|
+
|
|
116
|
+
mitmdump = self.find_mitmdump()
|
|
117
|
+
if not mitmdump:
|
|
118
|
+
raise RuntimeError(
|
|
119
|
+
"mitmdump not found. Install mitmproxy: pip install mitmproxy"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
listen_port = port or find_available_port(start_port=8090, end_port=9010)
|
|
123
|
+
if listen_port is None:
|
|
124
|
+
raise RuntimeError("No available port found in range 8090-9010")
|
|
125
|
+
|
|
126
|
+
addon_path = self._resolve_addon_path()
|
|
127
|
+
|
|
128
|
+
# Secure temp file for capture output
|
|
129
|
+
fd, output_path = tempfile.mkstemp(prefix="mitmproxy_", suffix=".json")
|
|
130
|
+
os.close(fd)
|
|
131
|
+
|
|
132
|
+
env = os.environ.copy()
|
|
133
|
+
env["MITMPROXY_TARGET"] = target
|
|
134
|
+
env["MITMPROXY_OUTPUT"] = output_path
|
|
135
|
+
env["MITMPROXY_MAX_REQ_BODY"] = str(max_req_body)
|
|
136
|
+
env["MITMPROXY_MAX_RES_BODY"] = str(max_res_body)
|
|
137
|
+
|
|
138
|
+
cmd_parts = [
|
|
139
|
+
mitmdump,
|
|
140
|
+
"-p",
|
|
141
|
+
str(listen_port),
|
|
142
|
+
"-s",
|
|
143
|
+
addon_path,
|
|
144
|
+
"--set",
|
|
145
|
+
"block_global=false",
|
|
146
|
+
"--ssl-insecure",
|
|
147
|
+
"--quiet",
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
# If mitmdump path is a shell expression (cd ... && uv run ...), use shell
|
|
151
|
+
use_shell = "&&" in mitmdump or ";" in mitmdump
|
|
152
|
+
if use_shell:
|
|
153
|
+
# First element is the shell prefix; remaining args are quoted individually
|
|
154
|
+
prefix = str(cmd_parts[0])
|
|
155
|
+
rest = " ".join(shlex.quote(str(p)) for p in cmd_parts[1:])
|
|
156
|
+
cmd = f"{prefix} {rest}"
|
|
157
|
+
else:
|
|
158
|
+
cmd = cmd_parts
|
|
159
|
+
|
|
160
|
+
emit_info(
|
|
161
|
+
f"🛡️ Starting mitmproxy on port {listen_port} (target: {target or 'all'})"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
if sys.platform.startswith("win"):
|
|
166
|
+
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
167
|
+
self._process = subprocess.Popen(
|
|
168
|
+
cmd,
|
|
169
|
+
shell=use_shell,
|
|
170
|
+
stdout=subprocess.DEVNULL,
|
|
171
|
+
stderr=subprocess.DEVNULL,
|
|
172
|
+
stdin=subprocess.DEVNULL,
|
|
173
|
+
env=env,
|
|
174
|
+
creationflags=creationflags,
|
|
175
|
+
)
|
|
176
|
+
else:
|
|
177
|
+
self._process = subprocess.Popen(
|
|
178
|
+
cmd,
|
|
179
|
+
shell=use_shell,
|
|
180
|
+
stdout=subprocess.DEVNULL,
|
|
181
|
+
stderr=subprocess.DEVNULL,
|
|
182
|
+
stdin=subprocess.DEVNULL,
|
|
183
|
+
env=env,
|
|
184
|
+
start_new_session=True,
|
|
185
|
+
)
|
|
186
|
+
except Exception as exc:
|
|
187
|
+
# Clean up temp file on failure
|
|
188
|
+
Path(output_path).unlink(missing_ok=True)
|
|
189
|
+
raise RuntimeError(f"Failed to start mitmdump: {exc}") from exc
|
|
190
|
+
|
|
191
|
+
self._port = listen_port
|
|
192
|
+
self._target = target
|
|
193
|
+
self._output_path = output_path
|
|
194
|
+
self._flow_count = 0
|
|
195
|
+
|
|
196
|
+
# Set proxy env vars so muse traffic routes through
|
|
197
|
+
proxy_url = f"http://127.0.0.1:{listen_port}"
|
|
198
|
+
self._saved_proxy_env = {
|
|
199
|
+
"HTTPS_PROXY": os.environ.get("HTTPS_PROXY"),
|
|
200
|
+
"HTTP_PROXY": os.environ.get("HTTP_PROXY"),
|
|
201
|
+
}
|
|
202
|
+
os.environ["HTTPS_PROXY"] = proxy_url
|
|
203
|
+
os.environ["HTTP_PROXY"] = proxy_url
|
|
204
|
+
|
|
205
|
+
# Give mitmdump a moment to bind
|
|
206
|
+
time.sleep(0.5)
|
|
207
|
+
|
|
208
|
+
if self._process.poll() is not None:
|
|
209
|
+
# Process exited immediately
|
|
210
|
+
exit_code = self._process.returncode
|
|
211
|
+
Path(output_path).unlink(missing_ok=True)
|
|
212
|
+
raise RuntimeError(f"mitmdump exited immediately (code {exit_code})")
|
|
213
|
+
|
|
214
|
+
emit_success(f"✅ mitmproxy running on port {listen_port}")
|
|
215
|
+
emit_info(
|
|
216
|
+
"⚠️ SSL verification is disabled (--ssl-insecure). "
|
|
217
|
+
"For production use, install and trust the mitmproxy CA certificate."
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
return listen_port
|
|
221
|
+
|
|
222
|
+
def stop(self) -> dict | None:
|
|
223
|
+
"""Stop mitmdump and read captured flows.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Dict with capture metadata and flows, or None if nothing captured.
|
|
227
|
+
"""
|
|
228
|
+
if self._process is None:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
# Send SIGTERM gracefully
|
|
232
|
+
self._terminate_process()
|
|
233
|
+
|
|
234
|
+
# Read capture file
|
|
235
|
+
data = self._read_capture_file()
|
|
236
|
+
if data:
|
|
237
|
+
self._flow_count = data.get("meta", {}).get("total_flows", 0)
|
|
238
|
+
|
|
239
|
+
# Restore or clear proxy env vars
|
|
240
|
+
for key, value in self._saved_proxy_env.items():
|
|
241
|
+
if value is None:
|
|
242
|
+
os.environ.pop(key, None)
|
|
243
|
+
else:
|
|
244
|
+
os.environ[key] = value
|
|
245
|
+
self._saved_proxy_env = {}
|
|
246
|
+
|
|
247
|
+
# Clean up temp file
|
|
248
|
+
if self._output_path:
|
|
249
|
+
Path(self._output_path).unlink(missing_ok=True)
|
|
250
|
+
self._output_path = ""
|
|
251
|
+
|
|
252
|
+
self._process = None
|
|
253
|
+
self._port = None
|
|
254
|
+
|
|
255
|
+
return data
|
|
256
|
+
|
|
257
|
+
def status(self) -> dict:
|
|
258
|
+
"""Return current proxy status."""
|
|
259
|
+
running = self._process is not None and self._process.poll() is None
|
|
260
|
+
return {
|
|
261
|
+
"running": running,
|
|
262
|
+
"port": self._port,
|
|
263
|
+
"target": self._target,
|
|
264
|
+
"flow_count": self._flow_count,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
def cleanup(self) -> None:
|
|
268
|
+
"""Ensure process is killed and temp files removed."""
|
|
269
|
+
if self._process is not None and self._process.poll() is None:
|
|
270
|
+
self._terminate_process()
|
|
271
|
+
if self._output_path:
|
|
272
|
+
Path(self._output_path).unlink(missing_ok=True)
|
|
273
|
+
self._output_path = ""
|
|
274
|
+
self._process = None
|
|
275
|
+
self._port = None
|
|
276
|
+
for key, value in self._saved_proxy_env.items():
|
|
277
|
+
if value is None:
|
|
278
|
+
os.environ.pop(key, None)
|
|
279
|
+
else:
|
|
280
|
+
os.environ[key] = value
|
|
281
|
+
self._saved_proxy_env = {}
|
|
282
|
+
|
|
283
|
+
def _terminate_process(self) -> None:
|
|
284
|
+
"""Best-effort graceful termination of the mitmdump process."""
|
|
285
|
+
proc = self._process
|
|
286
|
+
if proc is None:
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
if sys.platform.startswith("win"):
|
|
291
|
+
proc.terminate()
|
|
292
|
+
proc.wait(timeout=3)
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
# POSIX: start_new_session created a new process group
|
|
296
|
+
pid = proc.pid
|
|
297
|
+
try:
|
|
298
|
+
pgid = os.getpgid(pid)
|
|
299
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
300
|
+
time.sleep(1.0)
|
|
301
|
+
if proc.poll() is None:
|
|
302
|
+
os.killpg(pgid, signal.SIGINT)
|
|
303
|
+
time.sleep(0.5)
|
|
304
|
+
if proc.poll() is None:
|
|
305
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
306
|
+
proc.wait(timeout=2)
|
|
307
|
+
except OSError:
|
|
308
|
+
# Process may already be gone
|
|
309
|
+
try:
|
|
310
|
+
proc.kill()
|
|
311
|
+
proc.wait(timeout=1)
|
|
312
|
+
except Exception:
|
|
313
|
+
pass
|
|
314
|
+
except Exception as exc:
|
|
315
|
+
logger.warning("Error terminating mitmdump: %s", exc)
|
|
316
|
+
|
|
317
|
+
def _read_capture_file(self) -> dict | None:
|
|
318
|
+
"""Read and return the JSON capture file contents."""
|
|
319
|
+
if not self._output_path or not Path(self._output_path).exists():
|
|
320
|
+
return None
|
|
321
|
+
try:
|
|
322
|
+
with open(self._output_path, encoding="utf-8") as f:
|
|
323
|
+
return json.load(f)
|
|
324
|
+
except Exception as exc:
|
|
325
|
+
logger.warning("Failed to read capture file: %s", exc)
|
|
326
|
+
return None
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Register mitmproxy startup callbacks."""
|
|
2
|
+
|
|
3
|
+
from code_muse.callbacks import register_callback
|
|
4
|
+
from code_muse.messaging import emit_info, emit_warning
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _get_mitmproxy_prompt() -> str:
|
|
8
|
+
"""Return mitmproxy usage instructions for the agent prompt."""
|
|
9
|
+
return """## mitmproxy — Network Traffic Capture Tool
|
|
10
|
+
|
|
11
|
+
You have access to the `mitmproxy` tool that can intercept and inspect HTTP/S traffic.
|
|
12
|
+
|
|
13
|
+
**When to use it:**
|
|
14
|
+
- When a website, API endpoint, or web service isn't working as expected
|
|
15
|
+
- When you need to see exactly what data is being sent to/from a provider
|
|
16
|
+
- When debugging token counts, request formats, or response shapes
|
|
17
|
+
- When you want to verify headers, request bodies, or response payloads
|
|
18
|
+
- Any web-related debugging where seeing the raw traffic helps
|
|
19
|
+
|
|
20
|
+
**How to use it:**
|
|
21
|
+
1. Call `mitmproxy(command="start", target_domain="example.com")` to begin capture
|
|
22
|
+
2. Perform the actions/requests you want to inspect
|
|
23
|
+
3. Call `mitmproxy(command="stop")` to stop capture and get the recorded data
|
|
24
|
+
4. Or use `mitmproxy(command="capture", duration_seconds=30)` for a one-shot capture
|
|
25
|
+
|
|
26
|
+
**Pro tip:** Set `target_domain` to filter traffic (e.g., "anthropic.com",
|
|
27
|
+
"openai.com"). Leave it empty to capture all traffic. The proxy runs on localhost
|
|
28
|
+
and routes muse's own HTTP clients through it automatically.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _check_mitmproxy_available() -> None:
|
|
33
|
+
"""Verify mitmproxy is installed and available on startup."""
|
|
34
|
+
try:
|
|
35
|
+
import mitmproxy # noqa: F401
|
|
36
|
+
from mitmproxy.tools.main import mitmdump # noqa: F401
|
|
37
|
+
|
|
38
|
+
emit_info("🔍 mitmproxy available — mitmproxy detected")
|
|
39
|
+
except ImportError:
|
|
40
|
+
emit_warning("⚠️ mitmproxy not available. Install with: pip install mitmproxy")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def register() -> None:
|
|
44
|
+
register_callback("startup", _check_mitmproxy_available)
|
|
45
|
+
register_callback("load_prompt", _get_mitmproxy_prompt)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Workspace and sensitive-file path policy for Muse.
|
|
2
|
+
|
|
3
|
+
Provides resolve, classify, and check functions that enforce:
|
|
4
|
+
- Workspace containment (cwd-based root)
|
|
5
|
+
- Path traversal and symlink escape detection
|
|
6
|
+
- Sensitive file/directory blocking
|
|
7
|
+
- Operation-level gating (READ, LIST, SEARCH, WRITE, DELETE)
|
|
8
|
+
|
|
9
|
+
All file tools should call ``check_path_allowed()`` before opening/spawning
|
|
10
|
+
operations. Denials never include full file content.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
from enum import Enum, auto
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import NamedTuple
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Operation(Enum):
|
|
20
|
+
READ = auto()
|
|
21
|
+
LIST = auto()
|
|
22
|
+
SEARCH = auto()
|
|
23
|
+
WRITE = auto()
|
|
24
|
+
DELETE = auto()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PathDecision(NamedTuple):
|
|
28
|
+
allowed: bool
|
|
29
|
+
reason: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Files and directories that are considered sensitive and require explicit approval
|
|
33
|
+
SENSITIVE_PATHS: set[str] = {
|
|
34
|
+
".env",
|
|
35
|
+
".env.local",
|
|
36
|
+
".env.production",
|
|
37
|
+
".envrc",
|
|
38
|
+
"id_rsa",
|
|
39
|
+
"id_dsa",
|
|
40
|
+
"id_ecdsa",
|
|
41
|
+
"id_ed25519",
|
|
42
|
+
"authorized_keys",
|
|
43
|
+
"known_hosts",
|
|
44
|
+
"ssh_config",
|
|
45
|
+
".aws",
|
|
46
|
+
".gnupg",
|
|
47
|
+
".ssh",
|
|
48
|
+
".docker",
|
|
49
|
+
".kube",
|
|
50
|
+
"kubeconfig",
|
|
51
|
+
"credentials",
|
|
52
|
+
"secrets",
|
|
53
|
+
".netrc",
|
|
54
|
+
".npmrc",
|
|
55
|
+
".pypirc",
|
|
56
|
+
"tokens",
|
|
57
|
+
"cookie",
|
|
58
|
+
"cookies",
|
|
59
|
+
"keychain",
|
|
60
|
+
"keystore",
|
|
61
|
+
"private_key",
|
|
62
|
+
"private.pem",
|
|
63
|
+
"cert.pem",
|
|
64
|
+
".pgpass",
|
|
65
|
+
".mylogin.cnf",
|
|
66
|
+
"terraform.tfstate",
|
|
67
|
+
".terraform",
|
|
68
|
+
"service_account.json",
|
|
69
|
+
".htpasswd",
|
|
70
|
+
".htaccess",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Basenames that indicate a sensitive file regardless of path
|
|
74
|
+
_SENSITIVE_BASENAMES: set[str] = {
|
|
75
|
+
"passwd",
|
|
76
|
+
"shadow",
|
|
77
|
+
"sudoers",
|
|
78
|
+
"group",
|
|
79
|
+
"hosts",
|
|
80
|
+
"resolv.conf",
|
|
81
|
+
"fstab",
|
|
82
|
+
"crypttab",
|
|
83
|
+
"shadow-",
|
|
84
|
+
"passwd-",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_workspace_root() -> Path:
|
|
89
|
+
"""Return the workspace root for path containment checks.
|
|
90
|
+
|
|
91
|
+
Uses the current working directory as the default workspace root.
|
|
92
|
+
"""
|
|
93
|
+
return Path.cwd().resolve()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def resolve_user_path(file_path: str) -> Path:
|
|
97
|
+
"""Resolve a user-supplied path to an absolute, canonical path.
|
|
98
|
+
|
|
99
|
+
Expands ``~`` and resolves ``..`` / symlinks via ``Path.resolve()``.
|
|
100
|
+
"""
|
|
101
|
+
p = Path(file_path).expanduser()
|
|
102
|
+
try:
|
|
103
|
+
return p.resolve()
|
|
104
|
+
except OSError, RuntimeError:
|
|
105
|
+
# If resolve fails (e.g., permission denied), fall back to absolute
|
|
106
|
+
return p.absolute()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def classify_path(file_path: Path) -> dict[str, bool]:
|
|
110
|
+
"""Classify a resolved path into policy-relevant categories.
|
|
111
|
+
|
|
112
|
+
Returns a dict with:
|
|
113
|
+
- ``inside_workspace``: True if within the workspace root
|
|
114
|
+
- ``sensitive``: True if the path touches a sensitive file/dir
|
|
115
|
+
- ``traversal``: True if path contains ``..`` components (pre-resolve)
|
|
116
|
+
"""
|
|
117
|
+
workspace = get_workspace_root()
|
|
118
|
+
resolved = file_path
|
|
119
|
+
|
|
120
|
+
inside_workspace = False
|
|
121
|
+
try:
|
|
122
|
+
resolved.relative_to(workspace)
|
|
123
|
+
inside_workspace = True
|
|
124
|
+
except ValueError:
|
|
125
|
+
inside_workspace = False
|
|
126
|
+
|
|
127
|
+
sensitive = False
|
|
128
|
+
parts_lower = [part.lower() for part in resolved.parts]
|
|
129
|
+
basename_lower = resolved.name.lower()
|
|
130
|
+
|
|
131
|
+
for part in parts_lower:
|
|
132
|
+
if part in SENSITIVE_PATHS or part in _SENSITIVE_BASENAMES:
|
|
133
|
+
sensitive = True
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
if basename_lower in SENSITIVE_PATHS or basename_lower in _SENSITIVE_BASENAMES:
|
|
137
|
+
sensitive = True
|
|
138
|
+
|
|
139
|
+
# Symlink escape detection: if the resolved path differs from the
|
|
140
|
+
# absolute non-resolved path in a way that breaks containment.
|
|
141
|
+
traversal = False
|
|
142
|
+
try:
|
|
143
|
+
abs_no_resolve = Path(file_path).expanduser().absolute()
|
|
144
|
+
# If the path was expanded but resolve took us outside workspace,
|
|
145
|
+
# and the absolute path itself also looks outside, mark traversal.
|
|
146
|
+
if not inside_workspace:
|
|
147
|
+
try:
|
|
148
|
+
abs_no_resolve.relative_to(workspace)
|
|
149
|
+
except ValueError:
|
|
150
|
+
# Both resolved and absolute are outside workspace
|
|
151
|
+
pass
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
"inside_workspace": inside_workspace,
|
|
157
|
+
"sensitive": sensitive,
|
|
158
|
+
"traversal": traversal,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def check_path_allowed(
|
|
163
|
+
file_path: str,
|
|
164
|
+
operation: Operation,
|
|
165
|
+
approved_sensitive: Sequence[str] | None = None,
|
|
166
|
+
) -> PathDecision:
|
|
167
|
+
"""Check whether an operation on *file_path* is allowed by policy.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
file_path: The target path (may be relative, contain ``~``, etc.)
|
|
171
|
+
operation: The operation being attempted
|
|
172
|
+
approved_sensitive: Optional sequence of already-approved sensitive
|
|
173
|
+
paths (exact strings). Used when a user has explicitly approved
|
|
174
|
+
a previous read/search of a sensitive file.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
``PathDecision`` with ``allowed`` and an optional ``reason``.
|
|
178
|
+
"""
|
|
179
|
+
resolved = resolve_user_path(file_path)
|
|
180
|
+
classification = classify_path(resolved)
|
|
181
|
+
|
|
182
|
+
# Always block path traversal / symlink escapes for write/delete
|
|
183
|
+
if operation in (Operation.WRITE, Operation.DELETE) and classification["traversal"]:
|
|
184
|
+
return PathDecision(
|
|
185
|
+
allowed=False,
|
|
186
|
+
reason="Path traversal or symlink escape detected; write/delete blocked.",
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Outside workspace writes/deletes denied by default
|
|
190
|
+
if (
|
|
191
|
+
operation in (Operation.WRITE, Operation.DELETE)
|
|
192
|
+
and not classification["inside_workspace"]
|
|
193
|
+
):
|
|
194
|
+
return PathDecision(
|
|
195
|
+
allowed=False,
|
|
196
|
+
reason="Outside-workspace writes and deletes are denied by default.",
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Sensitive file reads/search require approval
|
|
200
|
+
if (
|
|
201
|
+
operation in (Operation.READ, Operation.LIST, Operation.SEARCH)
|
|
202
|
+
and classification["sensitive"]
|
|
203
|
+
):
|
|
204
|
+
if approved_sensitive and str(resolved) in approved_sensitive:
|
|
205
|
+
return PathDecision(allowed=True)
|
|
206
|
+
return PathDecision(
|
|
207
|
+
allowed=False,
|
|
208
|
+
reason="Sensitive file or directory access requires explicit user approval.",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Outside-workspace reads/search: warn but allow (existing semantics)
|
|
212
|
+
if (
|
|
213
|
+
operation in (Operation.READ, Operation.LIST, Operation.SEARCH)
|
|
214
|
+
and not classification["inside_workspace"]
|
|
215
|
+
):
|
|
216
|
+
# We allow but don't include content in denial messages
|
|
217
|
+
return PathDecision(allowed=True)
|
|
218
|
+
|
|
219
|
+
return PathDecision(allowed=True)
|