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
alysis_code/policy.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
import shlex
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class PolicyDecision:
|
|
10
|
+
allowed: bool
|
|
11
|
+
needs_confirm: bool
|
|
12
|
+
reason: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_DANGEROUS_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
16
|
+
(re.compile(r"\bmkfs(\.\w+)?\b"), "Filesystem formatting (mkfs)"),
|
|
17
|
+
(re.compile(r"\bwipefs\b"), "Filesystem wipe (wipefs)"),
|
|
18
|
+
(re.compile(r"\bdd\b.*\bof="), "Raw disk write (dd ... of=)"),
|
|
19
|
+
(re.compile(r"\b(parted|fdisk|sfdisk|sgdisk)\b"), "Disk partitioning tool"),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
_CONFIRM_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
|
23
|
+
(re.compile(r"\bgit\b\s+push\b"), "git push"),
|
|
24
|
+
(re.compile(r"\b(curl|wget)\b.*\|\s*(sh|bash)\b"), "pipe-to-shell installer"),
|
|
25
|
+
(re.compile(r"\bapt(-get)?\b\s+install\b"), "system package install"),
|
|
26
|
+
(re.compile(r"\bbrew\b\s+install\b"), "package install"),
|
|
27
|
+
(re.compile(r"\bpip\b\s+install\b"), "python package install"),
|
|
28
|
+
(re.compile(r"\bnpm\b\s+install\b"), "node package install"),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _is_rm_command_token(token: str) -> bool:
|
|
33
|
+
if not token:
|
|
34
|
+
return False
|
|
35
|
+
return token.rsplit("/", 1)[-1] == "rm"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _is_control_operator(token: str) -> bool:
|
|
39
|
+
return token in {"&&", "||", ";", "|"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _is_destructive_rm(lowered_cmd: str) -> bool:
|
|
43
|
+
try:
|
|
44
|
+
tokens = shlex.split(lowered_cmd)
|
|
45
|
+
except ValueError:
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
for idx, token in enumerate(tokens):
|
|
49
|
+
if not _is_rm_command_token(token):
|
|
50
|
+
continue
|
|
51
|
+
has_recursive = False
|
|
52
|
+
has_force = False
|
|
53
|
+
|
|
54
|
+
for opt in tokens[idx + 1 :]:
|
|
55
|
+
if _is_control_operator(opt) or opt == "--":
|
|
56
|
+
break
|
|
57
|
+
if not opt.startswith("-") or opt == "-":
|
|
58
|
+
break
|
|
59
|
+
if opt.startswith("--"):
|
|
60
|
+
if opt == "--recursive" or opt.startswith("--recursive="):
|
|
61
|
+
has_recursive = True
|
|
62
|
+
if opt == "--force" or opt.startswith("--force="):
|
|
63
|
+
has_force = True
|
|
64
|
+
else:
|
|
65
|
+
flags = opt[1:]
|
|
66
|
+
if "r" in flags or "R" in flags:
|
|
67
|
+
has_recursive = True
|
|
68
|
+
if "f" in flags:
|
|
69
|
+
has_force = True
|
|
70
|
+
if has_recursive and has_force:
|
|
71
|
+
return True
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def evaluate_shell_command(cmd: str) -> PolicyDecision:
|
|
76
|
+
c = cmd.strip()
|
|
77
|
+
if not c:
|
|
78
|
+
return PolicyDecision(allowed=False, needs_confirm=False, reason="Empty command")
|
|
79
|
+
|
|
80
|
+
lowered = c.lower()
|
|
81
|
+
|
|
82
|
+
if _is_destructive_rm(lowered):
|
|
83
|
+
return PolicyDecision(
|
|
84
|
+
allowed=False, needs_confirm=False, reason="Destructive remove (rm -rf)"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
for pat, reason in _DANGEROUS_PATTERNS:
|
|
88
|
+
if pat.search(lowered):
|
|
89
|
+
return PolicyDecision(allowed=False, needs_confirm=False, reason=reason)
|
|
90
|
+
|
|
91
|
+
for pat, reason in _CONFIRM_PATTERNS:
|
|
92
|
+
if pat.search(lowered):
|
|
93
|
+
return PolicyDecision(allowed=True, needs_confirm=True, reason=reason)
|
|
94
|
+
|
|
95
|
+
return PolicyDecision(allowed=True, needs_confirm=False, reason="OK")
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import http.cookies
|
|
6
|
+
import ipaddress
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import platform
|
|
10
|
+
import queue
|
|
11
|
+
import re
|
|
12
|
+
import secrets
|
|
13
|
+
import shutil
|
|
14
|
+
import socket
|
|
15
|
+
import subprocess
|
|
16
|
+
import threading
|
|
17
|
+
from functools import partial
|
|
18
|
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
|
|
22
|
+
|
|
23
|
+
PREVIEW_ACCESS_SCOPES = frozenset({"local", "lan"})
|
|
24
|
+
_TOKEN_QUERY_PARAMETER = "alysis_token"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class WorkspacePreviewHandler(SimpleHTTPRequestHandler):
|
|
28
|
+
"""Static handler constrained to one workspace directory."""
|
|
29
|
+
|
|
30
|
+
server_version = "AlysisPreview/2"
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
*args: Any,
|
|
35
|
+
directory: str,
|
|
36
|
+
auth_token: str | None = None,
|
|
37
|
+
**kwargs: Any,
|
|
38
|
+
) -> None:
|
|
39
|
+
self._preview_root = Path(directory).resolve()
|
|
40
|
+
self._auth_token = auth_token
|
|
41
|
+
digest = hashlib.sha256((auth_token or "local").encode("utf-8")).hexdigest()[:12]
|
|
42
|
+
self._auth_cookie_name = f"alysis_preview_{digest}"
|
|
43
|
+
super().__init__(*args, directory=os.fspath(self._preview_root), **kwargs)
|
|
44
|
+
|
|
45
|
+
def do_GET(self) -> None: # noqa: N802 - inherited HTTP handler API
|
|
46
|
+
if not self._authorize_request():
|
|
47
|
+
return
|
|
48
|
+
super().do_GET()
|
|
49
|
+
|
|
50
|
+
def do_HEAD(self) -> None: # noqa: N802 - inherited HTTP handler API
|
|
51
|
+
if not self._authorize_request():
|
|
52
|
+
return
|
|
53
|
+
super().do_HEAD()
|
|
54
|
+
|
|
55
|
+
def _authorize_request(self) -> bool:
|
|
56
|
+
if self._auth_token is None:
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
parsed = urlsplit(self.path)
|
|
60
|
+
query_items = parse_qsl(parsed.query, keep_blank_values=True)
|
|
61
|
+
supplied = next(
|
|
62
|
+
(value for key, value in query_items if key == _TOKEN_QUERY_PARAMETER), None
|
|
63
|
+
)
|
|
64
|
+
if supplied is not None and secrets.compare_digest(supplied, self._auth_token):
|
|
65
|
+
cleaned_query = urlencode(
|
|
66
|
+
[(key, value) for key, value in query_items if key != _TOKEN_QUERY_PARAMETER]
|
|
67
|
+
)
|
|
68
|
+
location = urlunsplit(("", "", parsed.path or "/", cleaned_query, ""))
|
|
69
|
+
self.send_response(303)
|
|
70
|
+
self.send_header("Location", location)
|
|
71
|
+
self.send_header(
|
|
72
|
+
"Set-Cookie",
|
|
73
|
+
f"{self._auth_cookie_name}={self._auth_token}; Path=/; HttpOnly; SameSite=Strict",
|
|
74
|
+
)
|
|
75
|
+
self.end_headers()
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
cookies = http.cookies.SimpleCookie()
|
|
79
|
+
try:
|
|
80
|
+
cookies.load(self.headers.get("Cookie", ""))
|
|
81
|
+
except http.cookies.CookieError:
|
|
82
|
+
cookies.clear()
|
|
83
|
+
morsel = cookies.get(self._auth_cookie_name)
|
|
84
|
+
if morsel is not None and secrets.compare_digest(morsel.value, self._auth_token):
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
body = b"Authentication is required for this network preview.\n"
|
|
88
|
+
self.send_response(401)
|
|
89
|
+
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
|
90
|
+
self.send_header("Content-Length", str(len(body)))
|
|
91
|
+
self.end_headers()
|
|
92
|
+
if self.command != "HEAD":
|
|
93
|
+
self.wfile.write(body)
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def send_head(self): # type: ignore[no-untyped-def]
|
|
97
|
+
translated = Path(self.translate_path(self.path))
|
|
98
|
+
try:
|
|
99
|
+
requested_relative = translated.relative_to(self._preview_root)
|
|
100
|
+
except ValueError:
|
|
101
|
+
self.send_error(403, "Path escapes the preview root")
|
|
102
|
+
return None
|
|
103
|
+
if any(part.startswith(".") for part in requested_relative.parts):
|
|
104
|
+
self.send_error(403, "Hidden files are not available in previews")
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
candidate = translated.resolve()
|
|
108
|
+
try:
|
|
109
|
+
candidate.relative_to(self._preview_root)
|
|
110
|
+
except ValueError:
|
|
111
|
+
self.send_error(403, "Path escapes the preview root")
|
|
112
|
+
return None
|
|
113
|
+
if candidate.is_dir():
|
|
114
|
+
for index_name in ("index.html", "index.htm"):
|
|
115
|
+
index_path = candidate / index_name
|
|
116
|
+
if not index_path.exists():
|
|
117
|
+
continue
|
|
118
|
+
try:
|
|
119
|
+
index_path.resolve().relative_to(self._preview_root)
|
|
120
|
+
except ValueError:
|
|
121
|
+
self.send_error(403, "Index file escapes the preview root")
|
|
122
|
+
return None
|
|
123
|
+
break
|
|
124
|
+
return super().send_head()
|
|
125
|
+
|
|
126
|
+
def list_directory(self, path: str): # type: ignore[no-untyped-def]
|
|
127
|
+
_ = path
|
|
128
|
+
self.send_error(403, "Directory listing is disabled")
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def end_headers(self) -> None:
|
|
132
|
+
self.send_header("Cache-Control", "no-store")
|
|
133
|
+
self.send_header("X-Content-Type-Options", "nosniff")
|
|
134
|
+
self.send_header("Referrer-Policy", "no-referrer")
|
|
135
|
+
super().end_headers()
|
|
136
|
+
|
|
137
|
+
def log_message(self, format: str, *args: Any) -> None:
|
|
138
|
+
# Access URLs contain a temporary credential. Do not copy request targets
|
|
139
|
+
# into durable logs where the token could outlive the preview process.
|
|
140
|
+
_ = (format, args)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class WorkspacePreviewHTTPServer(ThreadingHTTPServer):
|
|
144
|
+
daemon_threads = True
|
|
145
|
+
allow_reuse_address = True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def run_preview_server(
|
|
149
|
+
*,
|
|
150
|
+
root: Path,
|
|
151
|
+
access: str,
|
|
152
|
+
port: int = 0,
|
|
153
|
+
ready_file: Path,
|
|
154
|
+
token_file: Path | None = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
preview_root = root.expanduser().resolve()
|
|
157
|
+
if not preview_root.is_dir():
|
|
158
|
+
raise ValueError(f"Preview root is not a directory: {preview_root}")
|
|
159
|
+
normalized_access = str(access or "").strip().lower()
|
|
160
|
+
if normalized_access not in PREVIEW_ACCESS_SCOPES:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
f"Preview access must be one of: {', '.join(sorted(PREVIEW_ACCESS_SCOPES))}"
|
|
163
|
+
)
|
|
164
|
+
if isinstance(port, bool) or port < 0 or port > 65535:
|
|
165
|
+
raise ValueError("Preview port must be between 0 and 65535")
|
|
166
|
+
|
|
167
|
+
auth_token = _read_auth_token(token_file)
|
|
168
|
+
if normalized_access == "lan" and auth_token is None:
|
|
169
|
+
raise ValueError("LAN previews require a temporary authentication token")
|
|
170
|
+
handler = partial(
|
|
171
|
+
WorkspacePreviewHandler,
|
|
172
|
+
directory=os.fspath(preview_root),
|
|
173
|
+
auth_token=auth_token,
|
|
174
|
+
)
|
|
175
|
+
runtime = _runtime_environment()
|
|
176
|
+
server, family = _create_preview_server(
|
|
177
|
+
access=normalized_access,
|
|
178
|
+
port=port,
|
|
179
|
+
handler=handler,
|
|
180
|
+
runtime=runtime,
|
|
181
|
+
)
|
|
182
|
+
with server:
|
|
183
|
+
actual_port = int(server.server_address[1])
|
|
184
|
+
probe_host = _probe_host_for_family(family, actual_port)
|
|
185
|
+
preview_urls = _preview_urls(
|
|
186
|
+
access=normalized_access,
|
|
187
|
+
family=family,
|
|
188
|
+
port=actual_port,
|
|
189
|
+
probe_host=probe_host,
|
|
190
|
+
)
|
|
191
|
+
_write_ready_file(
|
|
192
|
+
ready_file,
|
|
193
|
+
{
|
|
194
|
+
"schema_version": 1,
|
|
195
|
+
"access": normalized_access,
|
|
196
|
+
"port": actual_port,
|
|
197
|
+
"probe_host": probe_host,
|
|
198
|
+
"preview_urls": preview_urls,
|
|
199
|
+
"runtime": runtime,
|
|
200
|
+
"authentication_required": auth_token is not None,
|
|
201
|
+
},
|
|
202
|
+
)
|
|
203
|
+
print(f"Alysis Code preview ready at {preview_urls[0]}", flush=True)
|
|
204
|
+
server.serve_forever(poll_interval=0.2)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _create_preview_server(
|
|
208
|
+
*,
|
|
209
|
+
access: str,
|
|
210
|
+
port: int,
|
|
211
|
+
handler: Any,
|
|
212
|
+
runtime: str,
|
|
213
|
+
) -> tuple[WorkspacePreviewHTTPServer, int]:
|
|
214
|
+
host: str | None = None if access == "lan" else "localhost"
|
|
215
|
+
flags = socket.AI_PASSIVE if access == "lan" else 0
|
|
216
|
+
candidates = socket.getaddrinfo(
|
|
217
|
+
host,
|
|
218
|
+
port,
|
|
219
|
+
family=socket.AF_UNSPEC,
|
|
220
|
+
type=socket.SOCK_STREAM,
|
|
221
|
+
proto=socket.IPPROTO_TCP,
|
|
222
|
+
flags=flags,
|
|
223
|
+
)
|
|
224
|
+
if access == "lan" or runtime == "wsl":
|
|
225
|
+
# IPv4 remains the most consistently forwarded family across WSL and
|
|
226
|
+
# consumer LAN routers. This orders system-provided candidates; it does
|
|
227
|
+
# not select or embed a machine-specific address.
|
|
228
|
+
candidates.sort(key=lambda candidate: candidate[0] != socket.AF_INET)
|
|
229
|
+
errors: list[str] = []
|
|
230
|
+
seen: set[tuple[int, tuple[Any, ...]]] = set()
|
|
231
|
+
for family, _socktype, _proto, _canonname, sockaddr in candidates:
|
|
232
|
+
if family not in {socket.AF_INET, socket.AF_INET6}:
|
|
233
|
+
continue
|
|
234
|
+
normalized_sockaddr = tuple(sockaddr)
|
|
235
|
+
key = (family, normalized_sockaddr)
|
|
236
|
+
if key in seen:
|
|
237
|
+
continue
|
|
238
|
+
seen.add(key)
|
|
239
|
+
server_type = type(
|
|
240
|
+
f"WorkspacePreviewHTTPServer_{family}",
|
|
241
|
+
(WorkspacePreviewHTTPServer,),
|
|
242
|
+
{"address_family": family},
|
|
243
|
+
)
|
|
244
|
+
try:
|
|
245
|
+
return server_type(sockaddr, handler), family
|
|
246
|
+
except OSError as exc:
|
|
247
|
+
errors.append(str(exc))
|
|
248
|
+
detail = "; ".join(dict.fromkeys(errors)) or "no usable TCP address was found"
|
|
249
|
+
raise OSError(f"Unable to bind the {access} preview server: {detail}")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _probe_host_for_family(family: int, port: int) -> str:
|
|
253
|
+
candidates = socket.getaddrinfo(
|
|
254
|
+
"localhost",
|
|
255
|
+
port,
|
|
256
|
+
family=family,
|
|
257
|
+
type=socket.SOCK_STREAM,
|
|
258
|
+
proto=socket.IPPROTO_TCP,
|
|
259
|
+
)
|
|
260
|
+
if not candidates:
|
|
261
|
+
raise OSError("The operating system did not provide a loopback address")
|
|
262
|
+
return str(candidates[0][4][0])
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _preview_urls(*, access: str, family: int, port: int, probe_host: str) -> list[str]:
|
|
266
|
+
if access == "local":
|
|
267
|
+
hosts = [probe_host, "localhost"]
|
|
268
|
+
else:
|
|
269
|
+
hosts = _discover_lan_hosts(family)
|
|
270
|
+
# A verified local URL remains useful when interface discovery is limited
|
|
271
|
+
# by the platform (and is the correct Windows-facing path under WSL
|
|
272
|
+
# localhost forwarding). Network addresses, when discoverable, stay first.
|
|
273
|
+
hosts.append(probe_host)
|
|
274
|
+
urls = [_http_url(host, port) for host in dict.fromkeys(hosts) if host]
|
|
275
|
+
if not urls:
|
|
276
|
+
raise OSError("Unable to determine a reachable preview URL")
|
|
277
|
+
return urls
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _discover_lan_hosts(family: int) -> list[str]:
|
|
281
|
+
hostname = socket.gethostname().strip()
|
|
282
|
+
hosts: list[str] = []
|
|
283
|
+
if hostname:
|
|
284
|
+
candidates = _bounded_getaddrinfo(
|
|
285
|
+
hostname,
|
|
286
|
+
None,
|
|
287
|
+
family=family,
|
|
288
|
+
type=socket.SOCK_STREAM,
|
|
289
|
+
)
|
|
290
|
+
for candidate in candidates:
|
|
291
|
+
host = str(candidate[4][0]).split("%", 1)[0]
|
|
292
|
+
try:
|
|
293
|
+
address = ipaddress.ip_address(host)
|
|
294
|
+
except ValueError:
|
|
295
|
+
continue
|
|
296
|
+
if address.is_loopback or address.is_unspecified or address.is_link_local:
|
|
297
|
+
continue
|
|
298
|
+
hosts.append(address.compressed)
|
|
299
|
+
hosts.extend(_discover_platform_lan_hosts(family))
|
|
300
|
+
return list(dict.fromkeys(hosts))
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _discover_platform_lan_hosts(family: int) -> list[str]:
|
|
304
|
+
system = platform.system().strip().lower()
|
|
305
|
+
candidates: list[str] = []
|
|
306
|
+
if system == "darwin" and family == socket.AF_INET:
|
|
307
|
+
route = shutil.which("route")
|
|
308
|
+
ipconfig = shutil.which("ipconfig")
|
|
309
|
+
if route and ipconfig:
|
|
310
|
+
route_output = _run_discovery_command([route, "-n", "get", "default"])
|
|
311
|
+
match = re.search(r"^\s*interface:\s*(\S+)\s*$", route_output, flags=re.MULTILINE)
|
|
312
|
+
if match:
|
|
313
|
+
address = _run_discovery_command(
|
|
314
|
+
[ipconfig, "getifaddr", match.group(1)],
|
|
315
|
+
).strip()
|
|
316
|
+
if address:
|
|
317
|
+
candidates.append(address)
|
|
318
|
+
elif system == "linux":
|
|
319
|
+
ip = shutil.which("ip")
|
|
320
|
+
if ip:
|
|
321
|
+
raw = _run_discovery_command([ip, "-j", "address", "show", "up"])
|
|
322
|
+
try:
|
|
323
|
+
interfaces = json.loads(raw)
|
|
324
|
+
except json.JSONDecodeError:
|
|
325
|
+
interfaces = []
|
|
326
|
+
if isinstance(interfaces, list):
|
|
327
|
+
expected_family = "inet" if family == socket.AF_INET else "inet6"
|
|
328
|
+
for interface in interfaces:
|
|
329
|
+
if not isinstance(interface, dict):
|
|
330
|
+
continue
|
|
331
|
+
for info in interface.get("addr_info") or []:
|
|
332
|
+
if not isinstance(info, dict) or info.get("family") != expected_family:
|
|
333
|
+
continue
|
|
334
|
+
if info.get("scope") != "global":
|
|
335
|
+
continue
|
|
336
|
+
candidates.append(str(info.get("local") or ""))
|
|
337
|
+
if not candidates:
|
|
338
|
+
hostname = shutil.which("hostname")
|
|
339
|
+
if hostname:
|
|
340
|
+
candidates.extend(_run_discovery_command([hostname, "-I"]).split())
|
|
341
|
+
|
|
342
|
+
discovered: list[str] = []
|
|
343
|
+
expected_version = 4 if family == socket.AF_INET else 6
|
|
344
|
+
for candidate in candidates:
|
|
345
|
+
try:
|
|
346
|
+
address = ipaddress.ip_address(candidate.split("%", 1)[0])
|
|
347
|
+
except ValueError:
|
|
348
|
+
continue
|
|
349
|
+
if address.version != expected_version:
|
|
350
|
+
continue
|
|
351
|
+
if address.is_loopback or address.is_unspecified or address.is_link_local:
|
|
352
|
+
continue
|
|
353
|
+
discovered.append(address.compressed)
|
|
354
|
+
return list(dict.fromkeys(discovered))
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _run_discovery_command(argv: list[str]) -> str:
|
|
358
|
+
try:
|
|
359
|
+
completed = subprocess.run(
|
|
360
|
+
argv,
|
|
361
|
+
check=False,
|
|
362
|
+
capture_output=True,
|
|
363
|
+
text=True,
|
|
364
|
+
timeout=0.75,
|
|
365
|
+
)
|
|
366
|
+
except (OSError, subprocess.SubprocessError):
|
|
367
|
+
return ""
|
|
368
|
+
return completed.stdout if completed.returncode == 0 else ""
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _bounded_getaddrinfo(
|
|
372
|
+
host: str,
|
|
373
|
+
port: int | None,
|
|
374
|
+
*,
|
|
375
|
+
family: int,
|
|
376
|
+
type: int,
|
|
377
|
+
timeout_s: float = 0.5,
|
|
378
|
+
) -> list[tuple[Any, ...]]:
|
|
379
|
+
results: queue.SimpleQueue[list[tuple[Any, ...]]] = queue.SimpleQueue()
|
|
380
|
+
|
|
381
|
+
def _resolve() -> None:
|
|
382
|
+
try:
|
|
383
|
+
resolved = socket.getaddrinfo(host, port, family=family, type=type)
|
|
384
|
+
except OSError:
|
|
385
|
+
resolved = []
|
|
386
|
+
results.put(resolved)
|
|
387
|
+
|
|
388
|
+
worker = threading.Thread(target=_resolve, name="preview-address-discovery", daemon=True)
|
|
389
|
+
worker.start()
|
|
390
|
+
worker.join(max(0.01, timeout_s))
|
|
391
|
+
if worker.is_alive():
|
|
392
|
+
return []
|
|
393
|
+
try:
|
|
394
|
+
return results.get_nowait()
|
|
395
|
+
except queue.Empty:
|
|
396
|
+
return []
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _http_url(host: str, port: int) -> str:
|
|
400
|
+
formatted_host = f"[{host}]" if ":" in host else host
|
|
401
|
+
return f"http://{formatted_host}:{port}"
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _read_auth_token(path: Path | None) -> str | None:
|
|
405
|
+
if path is None:
|
|
406
|
+
return None
|
|
407
|
+
token = path.read_text(encoding="utf-8").strip()
|
|
408
|
+
if len(token) < 32:
|
|
409
|
+
raise ValueError("Preview authentication token is invalid")
|
|
410
|
+
return token
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _write_ready_file(path: Path, payload: dict[str, Any]) -> None:
|
|
414
|
+
target = path.expanduser().resolve()
|
|
415
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
416
|
+
temporary = target.with_name(f".{target.name}.{os.getpid()}.tmp")
|
|
417
|
+
temporary.write_text(json.dumps(payload, sort_keys=True) + "\n", encoding="utf-8")
|
|
418
|
+
try:
|
|
419
|
+
temporary.chmod(0o600)
|
|
420
|
+
except OSError:
|
|
421
|
+
pass
|
|
422
|
+
temporary.replace(target)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _runtime_environment() -> str:
|
|
426
|
+
system = platform.system().strip().lower()
|
|
427
|
+
if system == "linux":
|
|
428
|
+
release = platform.release().lower()
|
|
429
|
+
proc_version = ""
|
|
430
|
+
try:
|
|
431
|
+
proc_version = Path("/proc/version").read_text(encoding="utf-8").lower()
|
|
432
|
+
except OSError:
|
|
433
|
+
pass
|
|
434
|
+
if "microsoft" in release or "microsoft" in proc_version or os.environ.get("WSL_INTEROP"):
|
|
435
|
+
return "wsl"
|
|
436
|
+
return system or "unknown"
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def main() -> None:
|
|
440
|
+
parser = argparse.ArgumentParser(description="Serve a workspace directory for local preview.")
|
|
441
|
+
parser.add_argument("--root", type=Path, required=True)
|
|
442
|
+
parser.add_argument("--access", choices=sorted(PREVIEW_ACCESS_SCOPES), required=True)
|
|
443
|
+
parser.add_argument("--port", type=int, default=0)
|
|
444
|
+
parser.add_argument("--ready-file", type=Path, required=True)
|
|
445
|
+
parser.add_argument("--token-file", type=Path)
|
|
446
|
+
args = parser.parse_args()
|
|
447
|
+
run_preview_server(
|
|
448
|
+
root=args.root,
|
|
449
|
+
access=args.access,
|
|
450
|
+
port=args.port,
|
|
451
|
+
ready_file=args.ready_file,
|
|
452
|
+
token_file=args.token_file,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
if __name__ == "__main__":
|
|
457
|
+
main()
|