alysis-code 0.13.0__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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""Machine-readable auth diagnostics for supervising applications.
|
|
2
|
+
|
|
3
|
+
A GUI that spawns the CLI cannot infer auth state from human console text, and a
|
|
4
|
+
spawned process does not necessarily see the same environment — most notably the
|
|
5
|
+
OS keyring — as the interactive shell the user logged in from. Everything here is
|
|
6
|
+
non-secret and safe to hand to a supervising app.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import sys
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .branding import env_get
|
|
19
|
+
from .config import config_path
|
|
20
|
+
from .mcp.token_store import (
|
|
21
|
+
KeyringOutcome,
|
|
22
|
+
TokenPayloadLoadResult,
|
|
23
|
+
fallback_key_source,
|
|
24
|
+
keyring_availability,
|
|
25
|
+
keyring_backend_name,
|
|
26
|
+
last_keyring_outcome,
|
|
27
|
+
load_token_payload_result,
|
|
28
|
+
planned_key_source,
|
|
29
|
+
stored_key_source,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
CONTEXT_INTERACTIVE = "interactive"
|
|
33
|
+
CONTEXT_SPAWNED = "spawned"
|
|
34
|
+
|
|
35
|
+
# A GUI-spawned process on macOS typically inherits only the system default
|
|
36
|
+
# PATH, which is how a CLI that works in a terminal can fail when spawned.
|
|
37
|
+
_MINIMAL_PATH_ENTRY_COUNT = 4
|
|
38
|
+
_CI_ENV_KEYS = ("CI", "GITHUB_ACTIONS", "BUILDKITE", "TEAMCITY_VERSION", "JENKINS_URL")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class CredentialStoreHealth:
|
|
43
|
+
"""Non-secret health of one encrypted credential store."""
|
|
44
|
+
|
|
45
|
+
name: str
|
|
46
|
+
path: str
|
|
47
|
+
exists: bool
|
|
48
|
+
readable: bool
|
|
49
|
+
key_source: str | None = None
|
|
50
|
+
planned_key_source: str | None = None
|
|
51
|
+
entry_count: int | None = None
|
|
52
|
+
legacy_plaintext: bool = False
|
|
53
|
+
error: str | None = None
|
|
54
|
+
|
|
55
|
+
def to_payload(self) -> dict[str, Any]:
|
|
56
|
+
return {
|
|
57
|
+
"name": self.name,
|
|
58
|
+
"path": self.path,
|
|
59
|
+
"exists": self.exists,
|
|
60
|
+
"readable": self.readable,
|
|
61
|
+
"key_source": self.key_source,
|
|
62
|
+
"planned_key_source": self.planned_key_source,
|
|
63
|
+
"entry_count": self.entry_count,
|
|
64
|
+
"legacy_plaintext": self.legacy_plaintext,
|
|
65
|
+
"error": self.error,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def provider_token_store_path() -> Path:
|
|
70
|
+
"""Return the native provider credential vault path."""
|
|
71
|
+
|
|
72
|
+
# Imported lazily: provider_auth.store imports the MCP token store, and a
|
|
73
|
+
# module-level import here would make auth diagnostics depend on the whole
|
|
74
|
+
# provider auth package just to name a file.
|
|
75
|
+
from .provider_auth.store import provider_token_store_path as _path
|
|
76
|
+
|
|
77
|
+
return _path()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def mcp_token_store_path() -> Path:
|
|
81
|
+
"""Return the MCP OAuth credential store path."""
|
|
82
|
+
|
|
83
|
+
from .mcp.oauth_store import mcp_oauth_token_store_path
|
|
84
|
+
|
|
85
|
+
return mcp_oauth_token_store_path()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def keyring_snapshot() -> dict[str, Any]:
|
|
89
|
+
"""Probe the OS keyring and describe what a credential write would use."""
|
|
90
|
+
|
|
91
|
+
probe = keyring_availability()
|
|
92
|
+
observed = last_keyring_outcome()
|
|
93
|
+
return {
|
|
94
|
+
"available": probe.available,
|
|
95
|
+
"backend": probe.backend,
|
|
96
|
+
"env_override": os.environ.get("PYTHON_KEYRING_BACKEND") or None,
|
|
97
|
+
"reason": probe.reason,
|
|
98
|
+
"error_type": probe.error_type,
|
|
99
|
+
"detail": probe.detail,
|
|
100
|
+
"observed_this_process": None if observed is None else _outcome_payload(observed),
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _outcome_payload(outcome: KeyringOutcome) -> dict[str, Any]:
|
|
105
|
+
return {
|
|
106
|
+
"available": outcome.available,
|
|
107
|
+
"reason": outcome.reason,
|
|
108
|
+
"error_type": outcome.error_type,
|
|
109
|
+
"backend": outcome.backend,
|
|
110
|
+
"detail": outcome.detail,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def credential_backend_fields() -> dict[str, Any]:
|
|
115
|
+
"""Return the keyring fields every auth status payload carries.
|
|
116
|
+
|
|
117
|
+
A supervising app needs these to explain why auth state differs between the
|
|
118
|
+
user's terminal and the process it spawned, instead of guessing.
|
|
119
|
+
|
|
120
|
+
A degradation actually hit while serving this command is ground truth and wins
|
|
121
|
+
over the backend classification. Absent that, the classification is used
|
|
122
|
+
without a read probe, so reporting auth state never risks prompting the user
|
|
123
|
+
to unlock a keyring.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
observed = last_keyring_outcome()
|
|
127
|
+
if observed is not None and not observed.available:
|
|
128
|
+
probe = observed
|
|
129
|
+
else:
|
|
130
|
+
probe = keyring_availability(probe_read=False)
|
|
131
|
+
return {
|
|
132
|
+
"keyring_available": probe.available,
|
|
133
|
+
"keyring_backend": probe.backend or keyring_backend_name(),
|
|
134
|
+
"credential_fallback": None if probe.available else fallback_key_source(),
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def credential_store_health(name: str, path: Path) -> CredentialStoreHealth:
|
|
139
|
+
"""Report whether one encrypted credential store can actually be read."""
|
|
140
|
+
|
|
141
|
+
planned = planned_key_source()
|
|
142
|
+
if not path.exists():
|
|
143
|
+
return CredentialStoreHealth(
|
|
144
|
+
name=name,
|
|
145
|
+
path=str(path),
|
|
146
|
+
exists=False,
|
|
147
|
+
readable=True,
|
|
148
|
+
planned_key_source=planned,
|
|
149
|
+
entry_count=0,
|
|
150
|
+
)
|
|
151
|
+
stored = stored_key_source(path)
|
|
152
|
+
try:
|
|
153
|
+
result: TokenPayloadLoadResult = load_token_payload_result(path)
|
|
154
|
+
except Exception as exc: # noqa: BLE001 - store backends fail in OS-specific ways
|
|
155
|
+
return CredentialStoreHealth(
|
|
156
|
+
name=name,
|
|
157
|
+
path=str(path),
|
|
158
|
+
exists=True,
|
|
159
|
+
readable=False,
|
|
160
|
+
key_source=stored,
|
|
161
|
+
planned_key_source=planned,
|
|
162
|
+
error=f"{exc.__class__.__name__}: {exc}",
|
|
163
|
+
)
|
|
164
|
+
return CredentialStoreHealth(
|
|
165
|
+
name=name,
|
|
166
|
+
path=str(path),
|
|
167
|
+
exists=True,
|
|
168
|
+
readable=True,
|
|
169
|
+
key_source=stored,
|
|
170
|
+
planned_key_source=planned,
|
|
171
|
+
entry_count=len(result.payload),
|
|
172
|
+
legacy_plaintext=result.legacy_plaintext,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def credential_store_healths() -> tuple[CredentialStoreHealth, ...]:
|
|
177
|
+
"""Report health for every encrypted credential store auth depends on."""
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
credential_store_health("provider_auth", provider_token_store_path()),
|
|
181
|
+
credential_store_health("mcp_oauth", mcp_token_store_path()),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _isatty(stream_name: str) -> bool:
|
|
186
|
+
stream = getattr(sys, stream_name, None)
|
|
187
|
+
if stream is None:
|
|
188
|
+
return False
|
|
189
|
+
try:
|
|
190
|
+
return bool(stream.isatty())
|
|
191
|
+
except Exception: # noqa: BLE001 - replaced streams need not implement isatty
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def context_snapshot() -> dict[str, Any]:
|
|
196
|
+
"""Describe whether this process looks interactive or spawned by another app."""
|
|
197
|
+
|
|
198
|
+
stdin_tty = _isatty("stdin")
|
|
199
|
+
stdout_tty = _isatty("stdout")
|
|
200
|
+
stderr_tty = _isatty("stderr")
|
|
201
|
+
ci = sorted(key for key in _CI_ENV_KEYS if os.environ.get(key))
|
|
202
|
+
kind = CONTEXT_INTERACTIVE if stdin_tty and stdout_tty else CONTEXT_SPAWNED
|
|
203
|
+
return {
|
|
204
|
+
"kind": kind,
|
|
205
|
+
"stdin_tty": stdin_tty,
|
|
206
|
+
"stdout_tty": stdout_tty,
|
|
207
|
+
"stderr_tty": stderr_tty,
|
|
208
|
+
"term": os.environ.get("TERM") or None,
|
|
209
|
+
"ci_env": ci,
|
|
210
|
+
"ssh_session": bool(os.environ.get("SSH_TTY") or os.environ.get("SSH_CONNECTION")),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _path_entries() -> list[str]:
|
|
215
|
+
raw = os.environ.get("PATH") or ""
|
|
216
|
+
return [entry for entry in raw.split(os.pathsep) if entry]
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def path_snapshot() -> dict[str, Any]:
|
|
220
|
+
"""Describe where the CLI was resolved from and whether PATH looks truncated."""
|
|
221
|
+
|
|
222
|
+
entries = _path_entries()
|
|
223
|
+
return {
|
|
224
|
+
"entry_count": len(entries),
|
|
225
|
+
"looks_minimal": len(entries) <= _MINIMAL_PATH_ENTRY_COUNT,
|
|
226
|
+
"resolved_cli": shutil.which("alysis"),
|
|
227
|
+
"argv0": (sys.argv[0] or None) if sys.argv else None,
|
|
228
|
+
"python_executable": sys.executable or None,
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def environment_snapshot() -> dict[str, Any]:
|
|
233
|
+
"""Report the auth-relevant environment a supervising app cannot see."""
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
"home": os.path.expanduser("~"),
|
|
237
|
+
"home_env": os.environ.get("HOME") or os.environ.get("USERPROFILE") or None,
|
|
238
|
+
"config_dir": str(config_path().parent),
|
|
239
|
+
"config_dir_override": env_get("ALYSIS_CONFIG_DIR") or None,
|
|
240
|
+
"config_path": str(config_path()),
|
|
241
|
+
"config_exists": config_path().exists(),
|
|
242
|
+
"platform": sys.platform,
|
|
243
|
+
"path": path_snapshot(),
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def auth_doctor_payload() -> dict[str, Any]:
|
|
248
|
+
"""Build the full ``alysis doctor auth --json`` report."""
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"context": context_snapshot(),
|
|
252
|
+
"environment": environment_snapshot(),
|
|
253
|
+
"keyring": keyring_snapshot(),
|
|
254
|
+
"credential_stores": [health.to_payload() for health in credential_store_healths()],
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
__all__ = [
|
|
259
|
+
"CONTEXT_INTERACTIVE",
|
|
260
|
+
"CONTEXT_SPAWNED",
|
|
261
|
+
"CredentialStoreHealth",
|
|
262
|
+
"auth_doctor_payload",
|
|
263
|
+
"context_snapshot",
|
|
264
|
+
"credential_backend_fields",
|
|
265
|
+
"credential_store_health",
|
|
266
|
+
"credential_store_healths",
|
|
267
|
+
"environment_snapshot",
|
|
268
|
+
"keyring_snapshot",
|
|
269
|
+
"mcp_token_store_path",
|
|
270
|
+
"path_snapshot",
|
|
271
|
+
"provider_token_store_path",
|
|
272
|
+
]
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import threading
|
|
8
|
+
import uuid
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Literal, Protocol
|
|
13
|
+
|
|
14
|
+
from .branding import default_sandbox_docker_image
|
|
15
|
+
from .config import AppConfig, ConfigError
|
|
16
|
+
from .sandbox_runner import (
|
|
17
|
+
_SENSITIVE_ENV_KEYS,
|
|
18
|
+
_build_bwrap_argv,
|
|
19
|
+
_build_docker_argv,
|
|
20
|
+
_docker_cleanup_container,
|
|
21
|
+
_emit_warning,
|
|
22
|
+
_supports_bwrap_unshare_cgroup,
|
|
23
|
+
)
|
|
24
|
+
from .sandbox_settings import ShellSandboxSettings, resolve_shell_sandbox_settings
|
|
25
|
+
from .service_persistence import apply_non_interactive_defaults
|
|
26
|
+
|
|
27
|
+
BackgroundTerminationMode = Literal["process_group", "direct"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class BackgroundProcessSpawn:
|
|
32
|
+
popen: subprocess.Popen[bytes]
|
|
33
|
+
cleanup: Callable[[], None]
|
|
34
|
+
started_argv: tuple[str, ...]
|
|
35
|
+
termination_mode: BackgroundTerminationMode = "process_group"
|
|
36
|
+
orphan_cleanup_kind: str | None = None
|
|
37
|
+
orphan_cleanup_id: str | None = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BackgroundShellRunner(Protocol):
|
|
41
|
+
def start(
|
|
42
|
+
self,
|
|
43
|
+
*,
|
|
44
|
+
root: Path,
|
|
45
|
+
cwd: Path,
|
|
46
|
+
cmd: str,
|
|
47
|
+
env_overrides: dict[str, str] | None = None,
|
|
48
|
+
) -> BackgroundProcessSpawn: ...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _background_env(env_overrides: dict[str, str] | None) -> dict[str, str]:
|
|
52
|
+
blocked = {key.upper() for key in _SENSITIVE_ENV_KEYS}
|
|
53
|
+
env = {key: value for key, value in os.environ.items() if key.upper() not in blocked}
|
|
54
|
+
return _apply_env_overrides(env, env_overrides)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _apply_env_overrides(
|
|
58
|
+
env: dict[str, str],
|
|
59
|
+
env_overrides: dict[str, str] | None,
|
|
60
|
+
) -> dict[str, str]:
|
|
61
|
+
blocked = {key.upper() for key in _SENSITIVE_ENV_KEYS}
|
|
62
|
+
# Defaults first so an explicit override still wins. Shared by the
|
|
63
|
+
# background runners and the durable-service manager, so this covers every
|
|
64
|
+
# non-foreground spawn in one place.
|
|
65
|
+
merged = apply_non_interactive_defaults(env)
|
|
66
|
+
for key, value in (env_overrides or {}).items():
|
|
67
|
+
if key.upper() in blocked:
|
|
68
|
+
continue
|
|
69
|
+
merged[key] = value
|
|
70
|
+
return merged
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _noop_cleanup() -> None:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class HostBackgroundRunner:
|
|
79
|
+
def start(
|
|
80
|
+
self,
|
|
81
|
+
*,
|
|
82
|
+
root: Path,
|
|
83
|
+
cwd: Path,
|
|
84
|
+
cmd: str,
|
|
85
|
+
env_overrides: dict[str, str] | None = None,
|
|
86
|
+
) -> BackgroundProcessSpawn:
|
|
87
|
+
kwargs: dict[str, object] = {}
|
|
88
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
89
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP
|
|
90
|
+
else:
|
|
91
|
+
kwargs["start_new_session"] = True
|
|
92
|
+
popen = subprocess.Popen(
|
|
93
|
+
cmd,
|
|
94
|
+
shell=True,
|
|
95
|
+
cwd=str(cwd),
|
|
96
|
+
stdin=subprocess.DEVNULL,
|
|
97
|
+
stdout=subprocess.PIPE,
|
|
98
|
+
stderr=subprocess.PIPE,
|
|
99
|
+
env=_background_env(env_overrides),
|
|
100
|
+
bufsize=0,
|
|
101
|
+
**kwargs,
|
|
102
|
+
)
|
|
103
|
+
return BackgroundProcessSpawn(
|
|
104
|
+
popen=popen,
|
|
105
|
+
cleanup=_noop_cleanup,
|
|
106
|
+
started_argv=(cmd,),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True)
|
|
111
|
+
class DisabledBackgroundRunner:
|
|
112
|
+
reason: str = "Background shell execution is disabled."
|
|
113
|
+
|
|
114
|
+
def start(
|
|
115
|
+
self,
|
|
116
|
+
*,
|
|
117
|
+
root: Path,
|
|
118
|
+
cwd: Path,
|
|
119
|
+
cmd: str,
|
|
120
|
+
env_overrides: dict[str, str] | None = None,
|
|
121
|
+
) -> BackgroundProcessSpawn:
|
|
122
|
+
raise RuntimeError(self.reason)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class LazyBackgroundShellRunner:
|
|
126
|
+
def __init__(self, loader: Callable[[], BackgroundShellRunner]) -> None:
|
|
127
|
+
self._loader = loader
|
|
128
|
+
self._runner: BackgroundShellRunner | None = None
|
|
129
|
+
self._load_error: Exception | None = None
|
|
130
|
+
self._lock = threading.Lock()
|
|
131
|
+
|
|
132
|
+
def _resolve_runner(self) -> BackgroundShellRunner:
|
|
133
|
+
if self._runner is not None:
|
|
134
|
+
return self._runner
|
|
135
|
+
if self._load_error is not None:
|
|
136
|
+
raise self._load_error
|
|
137
|
+
with self._lock:
|
|
138
|
+
if self._runner is not None:
|
|
139
|
+
return self._runner
|
|
140
|
+
if self._load_error is not None:
|
|
141
|
+
raise self._load_error
|
|
142
|
+
try:
|
|
143
|
+
runner = self._loader()
|
|
144
|
+
except Exception as exc:
|
|
145
|
+
self._load_error = exc
|
|
146
|
+
raise
|
|
147
|
+
self._runner = runner
|
|
148
|
+
return runner
|
|
149
|
+
|
|
150
|
+
def start(
|
|
151
|
+
self,
|
|
152
|
+
*,
|
|
153
|
+
root: Path,
|
|
154
|
+
cwd: Path,
|
|
155
|
+
cmd: str,
|
|
156
|
+
env_overrides: dict[str, str] | None = None,
|
|
157
|
+
) -> BackgroundProcessSpawn:
|
|
158
|
+
return self._resolve_runner().start(
|
|
159
|
+
root=root,
|
|
160
|
+
cwd=cwd,
|
|
161
|
+
cmd=cmd,
|
|
162
|
+
env_overrides=env_overrides,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass(frozen=True)
|
|
167
|
+
class BwrapBackgroundRunner:
|
|
168
|
+
network: str = "off"
|
|
169
|
+
clear_env: bool = True
|
|
170
|
+
profile: str = "hardened"
|
|
171
|
+
|
|
172
|
+
def start(
|
|
173
|
+
self,
|
|
174
|
+
*,
|
|
175
|
+
root: Path,
|
|
176
|
+
cwd: Path,
|
|
177
|
+
cmd: str,
|
|
178
|
+
env_overrides: dict[str, str] | None = None,
|
|
179
|
+
) -> BackgroundProcessSpawn:
|
|
180
|
+
root_abs = root.resolve()
|
|
181
|
+
argv, run_env = _build_bwrap_argv(
|
|
182
|
+
root=root_abs,
|
|
183
|
+
cwd=cwd,
|
|
184
|
+
cmd=cmd,
|
|
185
|
+
network=self.network,
|
|
186
|
+
clear_env=self.clear_env,
|
|
187
|
+
profile=self.profile,
|
|
188
|
+
unshare_cgroup=_supports_bwrap_unshare_cgroup(),
|
|
189
|
+
)
|
|
190
|
+
popen = subprocess.Popen(
|
|
191
|
+
argv,
|
|
192
|
+
stdin=subprocess.DEVNULL,
|
|
193
|
+
stdout=subprocess.PIPE,
|
|
194
|
+
stderr=subprocess.PIPE,
|
|
195
|
+
cwd=str(root_abs),
|
|
196
|
+
env=_apply_env_overrides(run_env, env_overrides),
|
|
197
|
+
bufsize=0,
|
|
198
|
+
start_new_session=True,
|
|
199
|
+
)
|
|
200
|
+
return BackgroundProcessSpawn(
|
|
201
|
+
popen=popen,
|
|
202
|
+
cleanup=_noop_cleanup,
|
|
203
|
+
started_argv=tuple(argv),
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@dataclass(frozen=True)
|
|
208
|
+
class DockerBackgroundRunner:
|
|
209
|
+
network: str = "off"
|
|
210
|
+
docker_image: str = default_sandbox_docker_image("dev")
|
|
211
|
+
clear_env: bool = True
|
|
212
|
+
pids_limit: int | None = None
|
|
213
|
+
memory_limit: str | None = None
|
|
214
|
+
cpus: str | None = None
|
|
215
|
+
read_only_rootfs: bool = False
|
|
216
|
+
protect_repo_meta: bool = True
|
|
217
|
+
env_allowlist: tuple[str, ...] = ()
|
|
218
|
+
warning_callback: Callable[[str], None] | None = None
|
|
219
|
+
|
|
220
|
+
def start(
|
|
221
|
+
self,
|
|
222
|
+
*,
|
|
223
|
+
root: Path,
|
|
224
|
+
cwd: Path,
|
|
225
|
+
cmd: str,
|
|
226
|
+
env_overrides: dict[str, str] | None = None,
|
|
227
|
+
) -> BackgroundProcessSpawn:
|
|
228
|
+
root_abs = root.resolve()
|
|
229
|
+
container_name = f"alysis-bgsbx-{uuid.uuid4().hex[:12]}"
|
|
230
|
+
argv, run_env = _build_docker_argv(
|
|
231
|
+
root=root_abs,
|
|
232
|
+
cwd=cwd,
|
|
233
|
+
cmd=cmd,
|
|
234
|
+
container_name=container_name,
|
|
235
|
+
network=self.network,
|
|
236
|
+
docker_image=self.docker_image,
|
|
237
|
+
clear_env=self.clear_env,
|
|
238
|
+
pids_limit=self.pids_limit,
|
|
239
|
+
memory_limit=self.memory_limit,
|
|
240
|
+
cpus=self.cpus,
|
|
241
|
+
read_only_rootfs=self.read_only_rootfs,
|
|
242
|
+
protect_repo_meta=self.protect_repo_meta,
|
|
243
|
+
env_allowlist=self.env_allowlist,
|
|
244
|
+
)
|
|
245
|
+
run_env = _apply_env_overrides(run_env, env_overrides)
|
|
246
|
+
|
|
247
|
+
def cleanup() -> None:
|
|
248
|
+
_docker_cleanup_container(
|
|
249
|
+
container_name,
|
|
250
|
+
cwd=str(root_abs),
|
|
251
|
+
env=run_env,
|
|
252
|
+
warning_callback=self.warning_callback,
|
|
253
|
+
reason="background process cleanup",
|
|
254
|
+
quiet=True,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
popen = subprocess.Popen(
|
|
258
|
+
argv,
|
|
259
|
+
stdin=subprocess.DEVNULL,
|
|
260
|
+
stdout=subprocess.PIPE,
|
|
261
|
+
stderr=subprocess.PIPE,
|
|
262
|
+
cwd=str(root_abs),
|
|
263
|
+
env=run_env,
|
|
264
|
+
bufsize=0,
|
|
265
|
+
)
|
|
266
|
+
return BackgroundProcessSpawn(
|
|
267
|
+
popen=popen,
|
|
268
|
+
cleanup=cleanup,
|
|
269
|
+
started_argv=tuple(argv),
|
|
270
|
+
termination_mode="direct",
|
|
271
|
+
orphan_cleanup_kind="docker-container",
|
|
272
|
+
orphan_cleanup_id=container_name,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def build_background_shell_runner_from_settings(
|
|
277
|
+
settings: ShellSandboxSettings,
|
|
278
|
+
root: Path,
|
|
279
|
+
warning_callback: Callable[[str], None] | None = None,
|
|
280
|
+
) -> BackgroundShellRunner:
|
|
281
|
+
_ = root.resolve()
|
|
282
|
+
|
|
283
|
+
if settings.mode == "off":
|
|
284
|
+
return HostBackgroundRunner()
|
|
285
|
+
|
|
286
|
+
is_linux = platform.system().lower() == "linux"
|
|
287
|
+
has_bwrap = is_linux and shutil.which("bwrap") is not None
|
|
288
|
+
has_docker = shutil.which("docker") is not None
|
|
289
|
+
|
|
290
|
+
def fallback_or_error(*, reason: str) -> BackgroundShellRunner:
|
|
291
|
+
if settings.mode == "warn":
|
|
292
|
+
disabled_reason = (
|
|
293
|
+
f"Background shell sandbox unavailable ({reason}); host fallback is disabled."
|
|
294
|
+
)
|
|
295
|
+
_emit_warning(disabled_reason, warning_callback=warning_callback)
|
|
296
|
+
return DisabledBackgroundRunner(reason=disabled_reason)
|
|
297
|
+
raise ConfigError(
|
|
298
|
+
"Shell sandbox strict mode is enabled, but no usable backend is available "
|
|
299
|
+
f"for background processes: {reason}. Install bubblewrap (Linux) or Docker, "
|
|
300
|
+
"or set ALYSIS_SHELL_SANDBOX_MODE=off for explicit unsafe host execution."
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if settings.backend == "auto":
|
|
304
|
+
if has_bwrap:
|
|
305
|
+
return BwrapBackgroundRunner(
|
|
306
|
+
network=settings.network,
|
|
307
|
+
clear_env=settings.clear_env,
|
|
308
|
+
profile=settings.bwrap_profile,
|
|
309
|
+
)
|
|
310
|
+
if has_docker:
|
|
311
|
+
return _docker_background_runner_from_settings(
|
|
312
|
+
settings,
|
|
313
|
+
warning_callback=warning_callback,
|
|
314
|
+
)
|
|
315
|
+
return fallback_or_error(reason="auto backend could not find bwrap or docker")
|
|
316
|
+
|
|
317
|
+
if settings.backend == "bwrap":
|
|
318
|
+
if has_bwrap:
|
|
319
|
+
return BwrapBackgroundRunner(
|
|
320
|
+
network=settings.network,
|
|
321
|
+
clear_env=settings.clear_env,
|
|
322
|
+
profile=settings.bwrap_profile,
|
|
323
|
+
)
|
|
324
|
+
return fallback_or_error(reason="bwrap backend selected, but bubblewrap is not available")
|
|
325
|
+
|
|
326
|
+
if settings.backend == "docker":
|
|
327
|
+
if has_docker:
|
|
328
|
+
return _docker_background_runner_from_settings(
|
|
329
|
+
settings,
|
|
330
|
+
warning_callback=warning_callback,
|
|
331
|
+
)
|
|
332
|
+
return fallback_or_error(reason="docker backend selected, but docker is not available")
|
|
333
|
+
|
|
334
|
+
raise ConfigError(f"Unhandled shell sandbox backend: {settings.backend}")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _docker_background_runner_from_settings(
|
|
338
|
+
settings: ShellSandboxSettings,
|
|
339
|
+
*,
|
|
340
|
+
warning_callback: Callable[[str], None] | None,
|
|
341
|
+
) -> DockerBackgroundRunner:
|
|
342
|
+
return DockerBackgroundRunner(
|
|
343
|
+
network=settings.network,
|
|
344
|
+
docker_image=settings.docker_image,
|
|
345
|
+
clear_env=settings.clear_env,
|
|
346
|
+
pids_limit=settings.docker_pids_limit,
|
|
347
|
+
memory_limit=settings.docker_memory,
|
|
348
|
+
cpus=settings.docker_cpus,
|
|
349
|
+
read_only_rootfs=settings.docker_read_only,
|
|
350
|
+
protect_repo_meta=settings.protect_repo_meta,
|
|
351
|
+
env_allowlist=settings.docker_env_allowlist,
|
|
352
|
+
warning_callback=warning_callback,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def build_background_shell_runner(
|
|
357
|
+
cfg: AppConfig,
|
|
358
|
+
root: Path,
|
|
359
|
+
warning_callback: Callable[[str], None] | None = None,
|
|
360
|
+
) -> BackgroundShellRunner:
|
|
361
|
+
settings = resolve_shell_sandbox_settings(cfg)
|
|
362
|
+
return build_background_shell_runner_from_settings(
|
|
363
|
+
settings,
|
|
364
|
+
root,
|
|
365
|
+
warning_callback=warning_callback,
|
|
366
|
+
)
|