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,682 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
import tempfile
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
from .branding import default_sandbox_docker_image, env_get
|
|
13
|
+
from .config import AppConfig
|
|
14
|
+
from .sandbox_runner import build_shell_runner_from_settings
|
|
15
|
+
from .sandbox_settings import ShellSandboxSettings, resolve_shell_sandbox_settings
|
|
16
|
+
|
|
17
|
+
SandboxCheckStatus = Literal["ok", "missing", "failed", "skipped", "disabled"]
|
|
18
|
+
|
|
19
|
+
_DOCKER_INFO_TIMEOUT_S = 10
|
|
20
|
+
_DOCKER_IMAGE_INSPECT_TIMEOUT_S = 20
|
|
21
|
+
_DOCKER_PULL_TIMEOUT_S = 900
|
|
22
|
+
_SANDBOX_SMOKE_TIMEOUT_S = 15
|
|
23
|
+
_READY_SMOKE_OUTPUT = "alysis-sandbox-ready"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class SandboxCheck:
|
|
28
|
+
name: str
|
|
29
|
+
status: SandboxCheckStatus
|
|
30
|
+
detail: str
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class SandboxDiagnostic:
|
|
35
|
+
ready: bool
|
|
36
|
+
status: str
|
|
37
|
+
configured_mode: str
|
|
38
|
+
configured_backend: str
|
|
39
|
+
selected_backend: str | None
|
|
40
|
+
docker_image: str
|
|
41
|
+
server_image: str
|
|
42
|
+
checks: tuple[SandboxCheck, ...]
|
|
43
|
+
next_steps: tuple[str, ...]
|
|
44
|
+
can_pull: bool = False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class SandboxImagePullResult:
|
|
49
|
+
image: str
|
|
50
|
+
ok: bool
|
|
51
|
+
output: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class SandboxPullResult:
|
|
56
|
+
ok: bool
|
|
57
|
+
results: tuple[SandboxImagePullResult, ...]
|
|
58
|
+
error: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class BubblewrapInstallPlan:
|
|
63
|
+
manager: str
|
|
64
|
+
command: tuple[str, ...]
|
|
65
|
+
display: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class BubblewrapInstallResult:
|
|
70
|
+
ok: bool
|
|
71
|
+
command: str
|
|
72
|
+
detail: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
_BWRAP_INSTALL_MANAGERS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
76
|
+
("apt-get", ("apt-get", "install", "-y", "bubblewrap")),
|
|
77
|
+
("dnf", ("dnf", "install", "-y", "bubblewrap")),
|
|
78
|
+
("pacman", ("pacman", "-S", "--noconfirm", "bubblewrap")),
|
|
79
|
+
("zypper", ("zypper", "--non-interactive", "install", "bubblewrap")),
|
|
80
|
+
("apk", ("apk", "add", "bubblewrap")),
|
|
81
|
+
)
|
|
82
|
+
_BWRAP_INSTALL_TIMEOUT_S = 300
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def default_sandbox_images(*, include_server: bool = True) -> tuple[str, ...]:
|
|
86
|
+
images = [default_sandbox_docker_image("dev")]
|
|
87
|
+
if include_server:
|
|
88
|
+
images.append(resolve_server_sandbox_image())
|
|
89
|
+
return tuple(dict.fromkeys(images))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def configured_sandbox_images(cfg: AppConfig, *, include_server: bool = True) -> tuple[str, ...]:
|
|
93
|
+
settings = resolve_shell_sandbox_settings(cfg)
|
|
94
|
+
images = [settings.docker_image]
|
|
95
|
+
if include_server:
|
|
96
|
+
images.append(resolve_server_sandbox_image())
|
|
97
|
+
return tuple(dict.fromkeys(images))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def resolve_server_sandbox_image() -> str:
|
|
101
|
+
return env_get(
|
|
102
|
+
"ALYSIS_SERVER_DOCKER_IMAGE",
|
|
103
|
+
default=env_get(
|
|
104
|
+
"ALYSIS_SHELL_SANDBOX_DOCKER_IMAGE",
|
|
105
|
+
default=default_sandbox_docker_image("server"),
|
|
106
|
+
),
|
|
107
|
+
) or default_sandbox_docker_image("server")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def diagnose_sandbox(
|
|
111
|
+
cfg: AppConfig,
|
|
112
|
+
*,
|
|
113
|
+
include_smoke: bool = False,
|
|
114
|
+
include_server_image: bool = True,
|
|
115
|
+
) -> SandboxDiagnostic:
|
|
116
|
+
settings = resolve_shell_sandbox_settings(cfg)
|
|
117
|
+
server_image = resolve_server_sandbox_image()
|
|
118
|
+
checks: list[SandboxCheck] = []
|
|
119
|
+
|
|
120
|
+
if settings.mode == "off":
|
|
121
|
+
checks.append(
|
|
122
|
+
SandboxCheck(
|
|
123
|
+
"sandbox mode",
|
|
124
|
+
"disabled",
|
|
125
|
+
"shell_sandbox.mode=off; commands will run on the host shell.",
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
return SandboxDiagnostic(
|
|
129
|
+
ready=True,
|
|
130
|
+
status="disabled",
|
|
131
|
+
configured_mode=settings.mode,
|
|
132
|
+
configured_backend=settings.backend,
|
|
133
|
+
selected_backend=None,
|
|
134
|
+
docker_image=settings.docker_image,
|
|
135
|
+
server_image=server_image,
|
|
136
|
+
checks=tuple(checks),
|
|
137
|
+
next_steps=(
|
|
138
|
+
"Sandboxing is disabled by configuration. Re-enable strict mode for safer command execution.",
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
selected_backend = _select_configured_backend(settings)
|
|
143
|
+
server_worker_backend = _resolve_server_worker_backend()
|
|
144
|
+
needs_server_docker_image = include_server_image and (
|
|
145
|
+
selected_backend == "docker" or server_worker_backend == "docker"
|
|
146
|
+
)
|
|
147
|
+
is_linux = _is_linux()
|
|
148
|
+
bwrap_path = shutil.which("bwrap") if is_linux else None
|
|
149
|
+
docker_path = shutil.which("docker")
|
|
150
|
+
|
|
151
|
+
if bwrap_path:
|
|
152
|
+
checks.append(SandboxCheck("bubblewrap", "ok", bwrap_path))
|
|
153
|
+
elif is_linux:
|
|
154
|
+
checks.append(
|
|
155
|
+
SandboxCheck("bubblewrap", "missing", "bubblewrap (`bwrap`) is not installed.")
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
checks.append(
|
|
159
|
+
SandboxCheck(
|
|
160
|
+
"bubblewrap",
|
|
161
|
+
"skipped",
|
|
162
|
+
"bubblewrap backend is only supported on Linux.",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
if docker_path:
|
|
167
|
+
checks.append(SandboxCheck("Docker CLI", "ok", docker_path))
|
|
168
|
+
else:
|
|
169
|
+
checks.append(
|
|
170
|
+
SandboxCheck("Docker CLI", "missing", "Docker is not installed or not on PATH.")
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if selected_backend is None:
|
|
174
|
+
checks.append(
|
|
175
|
+
SandboxCheck(
|
|
176
|
+
"selected backend",
|
|
177
|
+
"failed",
|
|
178
|
+
"No usable sandbox backend was found for shell_sandbox.backend=auto.",
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
return _diagnostic(
|
|
182
|
+
settings=settings,
|
|
183
|
+
server_image=server_image,
|
|
184
|
+
selected_backend=None,
|
|
185
|
+
checks=checks,
|
|
186
|
+
next_steps=_install_backend_steps(),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
checks.append(
|
|
190
|
+
SandboxCheck(
|
|
191
|
+
"selected backend",
|
|
192
|
+
"ok",
|
|
193
|
+
selected_backend,
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if selected_backend == "bwrap":
|
|
198
|
+
if not bwrap_path:
|
|
199
|
+
next_steps = (
|
|
200
|
+
_bwrap_linux_only_steps()
|
|
201
|
+
if _check_status(checks, "bubblewrap") == "skipped"
|
|
202
|
+
else _bwrap_missing_steps(settings=settings)
|
|
203
|
+
)
|
|
204
|
+
return _diagnostic(
|
|
205
|
+
settings=settings,
|
|
206
|
+
server_image=server_image,
|
|
207
|
+
selected_backend=selected_backend,
|
|
208
|
+
checks=checks,
|
|
209
|
+
next_steps=next_steps,
|
|
210
|
+
)
|
|
211
|
+
if include_smoke:
|
|
212
|
+
_append_smoke_check(checks=checks, settings=settings)
|
|
213
|
+
if needs_server_docker_image:
|
|
214
|
+
docker_info = _run_docker_info()
|
|
215
|
+
checks.append(docker_info)
|
|
216
|
+
if docker_info.status == "ok":
|
|
217
|
+
checks.append(_docker_image_check(server_image, label="server sandbox image"))
|
|
218
|
+
return _diagnostic(
|
|
219
|
+
settings=settings,
|
|
220
|
+
server_image=server_image,
|
|
221
|
+
selected_backend=selected_backend,
|
|
222
|
+
checks=checks,
|
|
223
|
+
next_steps=_next_steps_for_checks(checks, selected_backend=selected_backend),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
docker_info = _run_docker_info()
|
|
227
|
+
checks.append(docker_info)
|
|
228
|
+
if docker_info.status != "ok":
|
|
229
|
+
return _diagnostic(
|
|
230
|
+
settings=settings,
|
|
231
|
+
server_image=server_image,
|
|
232
|
+
selected_backend=selected_backend,
|
|
233
|
+
checks=checks,
|
|
234
|
+
next_steps=_docker_daemon_steps(docker_path=docker_path),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
checks.append(_docker_image_check(settings.docker_image, label="sandbox image"))
|
|
238
|
+
if needs_server_docker_image:
|
|
239
|
+
checks.append(_docker_image_check(server_image, label="server sandbox image"))
|
|
240
|
+
if include_smoke and _check_status(checks, "sandbox image") == "ok":
|
|
241
|
+
_append_smoke_check(checks=checks, settings=settings)
|
|
242
|
+
|
|
243
|
+
return _diagnostic(
|
|
244
|
+
settings=settings,
|
|
245
|
+
server_image=server_image,
|
|
246
|
+
selected_backend=selected_backend,
|
|
247
|
+
checks=checks,
|
|
248
|
+
next_steps=_next_steps_for_checks(checks, selected_backend=selected_backend),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def pull_sandbox_images(
|
|
253
|
+
images: tuple[str, ...] | list[str] | None = None,
|
|
254
|
+
*,
|
|
255
|
+
timeout_s: int = _DOCKER_PULL_TIMEOUT_S,
|
|
256
|
+
) -> SandboxPullResult:
|
|
257
|
+
selected_images = tuple(dict.fromkeys(images or default_sandbox_images()))
|
|
258
|
+
if not selected_images:
|
|
259
|
+
return SandboxPullResult(ok=True, results=())
|
|
260
|
+
|
|
261
|
+
docker_path = shutil.which("docker")
|
|
262
|
+
if not docker_path:
|
|
263
|
+
return SandboxPullResult(
|
|
264
|
+
ok=False,
|
|
265
|
+
results=(),
|
|
266
|
+
error=(
|
|
267
|
+
"Docker is not installed. Install Docker Desktop, open it, then run "
|
|
268
|
+
"`alysis sandbox pull` again."
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
docker_info = _run_docker_info()
|
|
273
|
+
if docker_info.status != "ok":
|
|
274
|
+
return SandboxPullResult(
|
|
275
|
+
ok=False,
|
|
276
|
+
results=(),
|
|
277
|
+
error=_docker_not_running_message(),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
results: list[SandboxImagePullResult] = []
|
|
281
|
+
for image in selected_images:
|
|
282
|
+
try:
|
|
283
|
+
proc = subprocess.run(
|
|
284
|
+
["docker", "pull", image],
|
|
285
|
+
check=False,
|
|
286
|
+
capture_output=True,
|
|
287
|
+
text=True,
|
|
288
|
+
timeout=timeout_s,
|
|
289
|
+
)
|
|
290
|
+
except subprocess.TimeoutExpired:
|
|
291
|
+
results.append(
|
|
292
|
+
SandboxImagePullResult(
|
|
293
|
+
image=image,
|
|
294
|
+
ok=False,
|
|
295
|
+
output=f"docker pull timed out after {timeout_s}s",
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
continue
|
|
299
|
+
except OSError as exc:
|
|
300
|
+
results.append(
|
|
301
|
+
SandboxImagePullResult(
|
|
302
|
+
image=image,
|
|
303
|
+
ok=False,
|
|
304
|
+
output=_compact_detail(str(exc)),
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
continue
|
|
308
|
+
output = _compact_detail(_combined_output(proc.stdout, proc.stderr), max_chars=4000)
|
|
309
|
+
results.append(
|
|
310
|
+
SandboxImagePullResult(
|
|
311
|
+
image=image,
|
|
312
|
+
ok=proc.returncode == 0,
|
|
313
|
+
output=output,
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
return SandboxPullResult(ok=all(item.ok for item in results), results=tuple(results))
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _needs_sudo() -> bool:
|
|
320
|
+
geteuid = getattr(os, "geteuid", None)
|
|
321
|
+
if not callable(geteuid):
|
|
322
|
+
return True
|
|
323
|
+
try:
|
|
324
|
+
return geteuid() != 0
|
|
325
|
+
except OSError:
|
|
326
|
+
return True
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def detect_bubblewrap_install_plan() -> BubblewrapInstallPlan | None:
|
|
330
|
+
"""Return how to install Bubblewrap on this host, or ``None`` if not applicable.
|
|
331
|
+
|
|
332
|
+
Returns ``None`` when not on Linux, when ``bwrap`` is already installed, when no
|
|
333
|
+
supported package manager is found, or when elevation is needed but ``sudo`` is
|
|
334
|
+
unavailable.
|
|
335
|
+
"""
|
|
336
|
+
if not _is_linux() or shutil.which("bwrap"):
|
|
337
|
+
return None
|
|
338
|
+
needs_sudo = _needs_sudo()
|
|
339
|
+
sudo_path = shutil.which("sudo")
|
|
340
|
+
if needs_sudo and not sudo_path:
|
|
341
|
+
return None
|
|
342
|
+
for manager, base_cmd in _BWRAP_INSTALL_MANAGERS:
|
|
343
|
+
if not shutil.which(manager):
|
|
344
|
+
continue
|
|
345
|
+
command = ("sudo", *base_cmd) if needs_sudo else base_cmd
|
|
346
|
+
return BubblewrapInstallPlan(
|
|
347
|
+
manager=manager,
|
|
348
|
+
command=command,
|
|
349
|
+
display=" ".join(command),
|
|
350
|
+
)
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def install_bubblewrap(
|
|
355
|
+
*,
|
|
356
|
+
plan: BubblewrapInstallPlan | None = None,
|
|
357
|
+
timeout_s: int = _BWRAP_INSTALL_TIMEOUT_S,
|
|
358
|
+
) -> BubblewrapInstallResult:
|
|
359
|
+
"""Install Bubblewrap via the host package manager.
|
|
360
|
+
|
|
361
|
+
The package manager runs with inherited stdio so an interactive ``sudo`` password
|
|
362
|
+
prompt and progress output reach the user directly.
|
|
363
|
+
"""
|
|
364
|
+
plan = plan or detect_bubblewrap_install_plan()
|
|
365
|
+
if plan is None:
|
|
366
|
+
return BubblewrapInstallResult(
|
|
367
|
+
ok=False,
|
|
368
|
+
command="",
|
|
369
|
+
detail=(
|
|
370
|
+
"No supported Linux package manager was found to install Bubblewrap "
|
|
371
|
+
"automatically. Install `bubblewrap` manually, or use Docker."
|
|
372
|
+
),
|
|
373
|
+
)
|
|
374
|
+
try:
|
|
375
|
+
proc = subprocess.run(list(plan.command), check=False, timeout=timeout_s)
|
|
376
|
+
except subprocess.TimeoutExpired:
|
|
377
|
+
return BubblewrapInstallResult(
|
|
378
|
+
ok=False,
|
|
379
|
+
command=plan.display,
|
|
380
|
+
detail=f"install timed out after {timeout_s}s",
|
|
381
|
+
)
|
|
382
|
+
except OSError as exc:
|
|
383
|
+
return BubblewrapInstallResult(ok=False, command=plan.display, detail=str(exc))
|
|
384
|
+
if proc.returncode != 0:
|
|
385
|
+
return BubblewrapInstallResult(
|
|
386
|
+
ok=False,
|
|
387
|
+
command=plan.display,
|
|
388
|
+
detail=f"package manager exited with code {proc.returncode}",
|
|
389
|
+
)
|
|
390
|
+
if not shutil.which("bwrap"):
|
|
391
|
+
return BubblewrapInstallResult(
|
|
392
|
+
ok=False,
|
|
393
|
+
command=plan.display,
|
|
394
|
+
detail="bubblewrap (`bwrap`) is still not on PATH after the install completed",
|
|
395
|
+
)
|
|
396
|
+
return BubblewrapInstallResult(ok=True, command=plan.display, detail="bubblewrap installed")
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def format_sandbox_problem_message(result: SandboxDiagnostic) -> str:
|
|
400
|
+
if result.ready:
|
|
401
|
+
if result.status == "disabled":
|
|
402
|
+
return "Alysis Code sandboxing is disabled by configuration."
|
|
403
|
+
return "Alysis Code sandbox is ready."
|
|
404
|
+
|
|
405
|
+
lines = [
|
|
406
|
+
"Alysis Code needs a safe runner before it can execute tests and shell commands.",
|
|
407
|
+
"",
|
|
408
|
+
"A safe runner isolates command execution from the rest of your computer.",
|
|
409
|
+
"",
|
|
410
|
+
"Status: not ready",
|
|
411
|
+
]
|
|
412
|
+
if result.selected_backend:
|
|
413
|
+
lines.append(f"Selected backend: {result.selected_backend}")
|
|
414
|
+
lines.append("")
|
|
415
|
+
lines.append("Next step:")
|
|
416
|
+
for step in result.next_steps:
|
|
417
|
+
lines.append(f"- {step}")
|
|
418
|
+
return "\n".join(lines)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _diagnostic(
|
|
422
|
+
*,
|
|
423
|
+
settings: ShellSandboxSettings,
|
|
424
|
+
server_image: str,
|
|
425
|
+
selected_backend: str | None,
|
|
426
|
+
checks: list[SandboxCheck],
|
|
427
|
+
next_steps: tuple[str, ...],
|
|
428
|
+
) -> SandboxDiagnostic:
|
|
429
|
+
ready = _checks_ready(checks, selected_backend=selected_backend)
|
|
430
|
+
can_pull = any(check.name.endswith("image") and check.status == "missing" for check in checks)
|
|
431
|
+
return SandboxDiagnostic(
|
|
432
|
+
ready=ready,
|
|
433
|
+
status="ready" if ready else "not_ready",
|
|
434
|
+
configured_mode=settings.mode,
|
|
435
|
+
configured_backend=settings.backend,
|
|
436
|
+
selected_backend=selected_backend,
|
|
437
|
+
docker_image=settings.docker_image,
|
|
438
|
+
server_image=server_image,
|
|
439
|
+
checks=tuple(checks),
|
|
440
|
+
next_steps=next_steps,
|
|
441
|
+
can_pull=can_pull,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _select_configured_backend(settings: ShellSandboxSettings) -> str | None:
|
|
446
|
+
if settings.backend == "bwrap":
|
|
447
|
+
return "bwrap"
|
|
448
|
+
if settings.backend == "docker":
|
|
449
|
+
return "docker"
|
|
450
|
+
|
|
451
|
+
if _is_linux() and shutil.which("bwrap"):
|
|
452
|
+
return "bwrap"
|
|
453
|
+
if shutil.which("docker"):
|
|
454
|
+
return "docker"
|
|
455
|
+
return None
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _is_linux() -> bool:
|
|
459
|
+
return platform.system().lower() == "linux"
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _resolve_server_worker_backend() -> str:
|
|
463
|
+
raw = str(env_get("ALYSIS_SERVER_WORKER_BACKEND") or "").strip().lower()
|
|
464
|
+
if raw in {"bwrap", "docker"}:
|
|
465
|
+
return raw
|
|
466
|
+
if _is_linux():
|
|
467
|
+
return "bwrap"
|
|
468
|
+
return "docker"
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _run_docker_info() -> SandboxCheck:
|
|
472
|
+
if not shutil.which("docker"):
|
|
473
|
+
return SandboxCheck("Docker daemon", "skipped", "Docker CLI is not installed.")
|
|
474
|
+
try:
|
|
475
|
+
proc = subprocess.run(
|
|
476
|
+
["docker", "info"],
|
|
477
|
+
check=False,
|
|
478
|
+
capture_output=True,
|
|
479
|
+
text=True,
|
|
480
|
+
timeout=_DOCKER_INFO_TIMEOUT_S,
|
|
481
|
+
)
|
|
482
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
483
|
+
return SandboxCheck("Docker daemon", "failed", str(exc))
|
|
484
|
+
if proc.returncode == 0:
|
|
485
|
+
return SandboxCheck("Docker daemon", "ok", "running")
|
|
486
|
+
return SandboxCheck(
|
|
487
|
+
"Docker daemon",
|
|
488
|
+
"failed",
|
|
489
|
+
_docker_info_failure_detail(stdout=proc.stdout, stderr=proc.stderr),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _docker_image_check(image: str, *, label: str) -> SandboxCheck:
|
|
494
|
+
try:
|
|
495
|
+
proc = subprocess.run(
|
|
496
|
+
["docker", "image", "inspect", image],
|
|
497
|
+
check=False,
|
|
498
|
+
capture_output=True,
|
|
499
|
+
text=True,
|
|
500
|
+
timeout=_DOCKER_IMAGE_INSPECT_TIMEOUT_S,
|
|
501
|
+
)
|
|
502
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
503
|
+
return SandboxCheck(label, "failed", str(exc))
|
|
504
|
+
if proc.returncode == 0:
|
|
505
|
+
return SandboxCheck(label, "ok", image)
|
|
506
|
+
return SandboxCheck(label, "missing", f"{image} is not downloaded locally.")
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _append_smoke_check(*, checks: list[SandboxCheck], settings: ShellSandboxSettings) -> None:
|
|
510
|
+
try:
|
|
511
|
+
with tempfile.TemporaryDirectory(prefix="alysis-sandbox-smoke-") as raw_tmp:
|
|
512
|
+
root = Path(raw_tmp)
|
|
513
|
+
runner = build_shell_runner_from_settings(settings, root, warning_callback=None)
|
|
514
|
+
result = runner.run(
|
|
515
|
+
root=root,
|
|
516
|
+
cwd=root,
|
|
517
|
+
cmd=f"printf {_READY_SMOKE_OUTPUT}",
|
|
518
|
+
timeout_s=_SANDBOX_SMOKE_TIMEOUT_S,
|
|
519
|
+
)
|
|
520
|
+
except (OSError, RuntimeError, subprocess.TimeoutExpired) as exc:
|
|
521
|
+
checks.append(SandboxCheck("sandbox smoke test", "failed", str(exc)))
|
|
522
|
+
return
|
|
523
|
+
output = _combined_output(result.stdout, result.stderr).strip()
|
|
524
|
+
if result.returncode == 0 and output == _READY_SMOKE_OUTPUT:
|
|
525
|
+
checks.append(SandboxCheck("sandbox smoke test", "ok", "command executed in sandbox"))
|
|
526
|
+
return
|
|
527
|
+
detail = output or f"exit code {result.returncode}"
|
|
528
|
+
checks.append(SandboxCheck("sandbox smoke test", "failed", detail))
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def _checks_ready(checks: list[SandboxCheck], *, selected_backend: str | None) -> bool:
|
|
532
|
+
if selected_backend is None:
|
|
533
|
+
return False
|
|
534
|
+
required_names = {"selected backend"}
|
|
535
|
+
if selected_backend == "bwrap":
|
|
536
|
+
required_names.add("bubblewrap")
|
|
537
|
+
elif selected_backend == "docker":
|
|
538
|
+
required_names.update({"Docker CLI", "Docker daemon", "sandbox image"})
|
|
539
|
+
if _check_status(checks, "Docker daemon") is not None:
|
|
540
|
+
required_names.update({"Docker CLI", "Docker daemon"})
|
|
541
|
+
if _check_status(checks, "server sandbox image") is not None:
|
|
542
|
+
required_names.add("server sandbox image")
|
|
543
|
+
if any(check.status == "failed" for check in checks):
|
|
544
|
+
return False
|
|
545
|
+
for check in checks:
|
|
546
|
+
if check.name in required_names and check.status != "ok":
|
|
547
|
+
return False
|
|
548
|
+
smoke_status = _check_status(checks, "sandbox smoke test")
|
|
549
|
+
return smoke_status in {None, "ok"}
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _check_status(checks: list[SandboxCheck], name: str) -> SandboxCheckStatus | None:
|
|
553
|
+
for check in checks:
|
|
554
|
+
if check.name == name:
|
|
555
|
+
return check.status
|
|
556
|
+
return None
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _next_steps_for_checks(
|
|
560
|
+
checks: list[SandboxCheck],
|
|
561
|
+
*,
|
|
562
|
+
selected_backend: str,
|
|
563
|
+
) -> tuple[str, ...]:
|
|
564
|
+
if _check_status(checks, "Docker daemon") not in {None, "ok"}:
|
|
565
|
+
return _docker_daemon_steps(docker_path=shutil.which("docker"))
|
|
566
|
+
|
|
567
|
+
if selected_backend == "bwrap":
|
|
568
|
+
if _check_status(checks, "bubblewrap") == "skipped":
|
|
569
|
+
return _bwrap_linux_only_steps()
|
|
570
|
+
if _check_status(checks, "bubblewrap") != "ok":
|
|
571
|
+
return _bwrap_missing_steps(settings=None)
|
|
572
|
+
if _check_status(checks, "sandbox smoke test") == "failed":
|
|
573
|
+
return (
|
|
574
|
+
"Bubblewrap is installed but the sandbox smoke test failed. Check kernel user namespace support or set ALYSIS_SHELL_SANDBOX_BACKEND=docker.",
|
|
575
|
+
"Run `alysis doctor sandbox` again after fixing the host sandbox runtime.",
|
|
576
|
+
)
|
|
577
|
+
if _check_status(checks, "server sandbox image") == "missing":
|
|
578
|
+
return (
|
|
579
|
+
"Run `alysis sandbox pull --server` to download Alysis Code's server worker image.",
|
|
580
|
+
)
|
|
581
|
+
return ("Sandbox is ready.",)
|
|
582
|
+
|
|
583
|
+
if _check_status(checks, "Docker CLI") != "ok":
|
|
584
|
+
return ("Install Docker Desktop, open it, then run `alysis doctor sandbox`.",)
|
|
585
|
+
if _check_status(checks, "Docker daemon") != "ok":
|
|
586
|
+
return _docker_daemon_steps(docker_path=shutil.which("docker"))
|
|
587
|
+
if _check_status(checks, "sandbox image") == "missing":
|
|
588
|
+
return ("Run `alysis sandbox pull` to download Alysis Code's safe runner image.",)
|
|
589
|
+
if _check_status(checks, "sandbox smoke test") == "failed":
|
|
590
|
+
return (
|
|
591
|
+
"Docker is running, but the sandbox smoke test failed. Run `alysis doctor sandbox` and inspect the smoke test detail.",
|
|
592
|
+
)
|
|
593
|
+
if _check_status(checks, "server sandbox image") == "missing":
|
|
594
|
+
return (
|
|
595
|
+
"Run `alysis sandbox pull --server` to download Alysis Code's server worker image.",
|
|
596
|
+
)
|
|
597
|
+
return ("Sandbox is ready.",)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _install_backend_steps() -> tuple[str, ...]:
|
|
601
|
+
if platform.system().lower() == "linux":
|
|
602
|
+
return (
|
|
603
|
+
"Install Bubblewrap with your Linux package manager, then run `alysis doctor sandbox`.",
|
|
604
|
+
"Or install Docker, start Docker, then run `alysis sandbox pull`.",
|
|
605
|
+
)
|
|
606
|
+
return ("Install Docker Desktop, open it, then run `alysis sandbox pull`.",)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _bwrap_missing_steps(settings: ShellSandboxSettings | None) -> tuple[str, ...]:
|
|
610
|
+
backend_note = ""
|
|
611
|
+
if settings is not None and settings.backend == "bwrap":
|
|
612
|
+
backend_note = " because shell_sandbox.backend=bwrap is configured"
|
|
613
|
+
return (
|
|
614
|
+
f"Install Bubblewrap (`bwrap`){backend_note}, then run `alysis doctor sandbox`.",
|
|
615
|
+
"Or set ALYSIS_SHELL_SANDBOX_BACKEND=docker and run `alysis sandbox pull`.",
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _bwrap_linux_only_steps() -> tuple[str, ...]:
|
|
620
|
+
return (
|
|
621
|
+
"Bubblewrap is Linux-only. Install Docker Desktop, set ALYSIS_SHELL_SANDBOX_BACKEND=docker, then run `alysis sandbox pull`.",
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _docker_daemon_steps(*, docker_path: str | None) -> tuple[str, ...]:
|
|
626
|
+
if docker_path:
|
|
627
|
+
return (
|
|
628
|
+
"Docker is installed, but it is not running. Open Docker Desktop or start the Docker service, then run `alysis doctor sandbox`.",
|
|
629
|
+
)
|
|
630
|
+
return ("Install Docker Desktop, open it, then run `alysis sandbox pull`.",)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def _docker_not_running_message() -> str:
|
|
634
|
+
return (
|
|
635
|
+
"Docker is installed, but it is not running. Open Docker Desktop or start the Docker "
|
|
636
|
+
"service, then run `alysis sandbox pull` again."
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _combined_output(stdout: str | None, stderr: str | None) -> str:
|
|
641
|
+
return "\n".join(part for part in ((stdout or "").strip(), (stderr or "").strip()) if part)
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
def _docker_info_failure_detail(*, stdout: str | None, stderr: str | None) -> str:
|
|
645
|
+
combined = _combined_output(stdout, stderr)
|
|
646
|
+
important_lines: list[str] = []
|
|
647
|
+
for raw_line in combined.splitlines():
|
|
648
|
+
line = raw_line.strip()
|
|
649
|
+
lowered = line.lower()
|
|
650
|
+
if any(
|
|
651
|
+
marker in lowered
|
|
652
|
+
for marker in (
|
|
653
|
+
"cannot connect",
|
|
654
|
+
"failed to connect",
|
|
655
|
+
"docker daemon",
|
|
656
|
+
"permission denied",
|
|
657
|
+
)
|
|
658
|
+
):
|
|
659
|
+
important_lines.append(line)
|
|
660
|
+
if important_lines:
|
|
661
|
+
return _compact_detail(" ".join(important_lines))
|
|
662
|
+
return _compact_detail(combined)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _compact_detail(value: str, *, max_chars: int = 700) -> str:
|
|
666
|
+
text = value.strip()
|
|
667
|
+
if not text:
|
|
668
|
+
return "(no output)"
|
|
669
|
+
if len(text) <= max_chars:
|
|
670
|
+
return text
|
|
671
|
+
return text[: max_chars - 3].rstrip() + "..."
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def sandbox_env_summary() -> dict[str, str | None]:
|
|
675
|
+
keys = (
|
|
676
|
+
"ALYSIS_SHELL_SANDBOX_MODE",
|
|
677
|
+
"ALYSIS_SHELL_SANDBOX_BACKEND",
|
|
678
|
+
"ALYSIS_SHELL_SANDBOX_NETWORK",
|
|
679
|
+
"ALYSIS_SHELL_SANDBOX_DOCKER_IMAGE",
|
|
680
|
+
"ALYSIS_VERIFY_SANDBOX_MODE",
|
|
681
|
+
)
|
|
682
|
+
return {key: os.environ.get(key) for key in keys}
|