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/tools/web.py
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import html
|
|
5
|
+
import ipaddress
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import socket
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urlsplit
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from ..safety import SafeHttpError, safe_http_request
|
|
16
|
+
from ..web_research import canonicalize_web_url_input, normalize_web_url
|
|
17
|
+
from .http_timeout import build_http_timeout_budget, format_http_timeout_error
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WebFetchError(RuntimeError):
|
|
21
|
+
"""Raised for any web_fetch failure.
|
|
22
|
+
|
|
23
|
+
``recoverable=True`` marks failures the calling model can fix by adjusting its
|
|
24
|
+
own tool arguments (input validation). The agent loop returns those to the
|
|
25
|
+
model as plain errors instead of disabling web tools for the rest of the turn.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, message: str, *, recoverable: bool = False) -> None:
|
|
29
|
+
super().__init__(message)
|
|
30
|
+
self.recoverable = recoverable
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_DEFAULT_MAX_CHARS = 20_000
|
|
34
|
+
_MAX_MAX_CHARS = 50_000
|
|
35
|
+
_MAX_REDIRECTS = 5
|
|
36
|
+
_DEFAULT_TIMEOUT_SECONDS = 10.0
|
|
37
|
+
_MAX_RESPONSE_BYTES = 1_000_000
|
|
38
|
+
_REDIRECT_STATUS_CODES = {301, 302, 303, 307, 308}
|
|
39
|
+
_HTML_CONTENT_TYPES = {"text/html", "application/xhtml+xml"}
|
|
40
|
+
_JSON_CONTENT_TYPES = {"application/json", "text/json"}
|
|
41
|
+
_XML_CONTENT_TYPES = {"application/xml", "text/xml"}
|
|
42
|
+
_EXTRA_TEXT_CONTENT_TYPES = {
|
|
43
|
+
"application/javascript",
|
|
44
|
+
"application/x-javascript",
|
|
45
|
+
"application/ecmascript",
|
|
46
|
+
"application/x-www-form-urlencoded",
|
|
47
|
+
"application/x-yaml",
|
|
48
|
+
"application/yaml",
|
|
49
|
+
"application/toml",
|
|
50
|
+
}
|
|
51
|
+
_BLOCKED_IPV4_NETWORKS = (
|
|
52
|
+
ipaddress.ip_network("100.64.0.0/10"),
|
|
53
|
+
ipaddress.ip_network("198.18.0.0/15"),
|
|
54
|
+
ipaddress.ip_network("169.254.169.254/32"),
|
|
55
|
+
ipaddress.ip_network("100.100.100.200/32"),
|
|
56
|
+
)
|
|
57
|
+
_TEXT_HEADERS = {
|
|
58
|
+
"User-Agent": (
|
|
59
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
|
60
|
+
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
|
|
61
|
+
),
|
|
62
|
+
"Accept": (
|
|
63
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,"
|
|
64
|
+
"application/json;q=0.8,text/plain;q=0.8,*/*;q=0.5"
|
|
65
|
+
),
|
|
66
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_TAG_BLOCK_RE = re.compile(r"(?is)<(script|style|noscript)\b.*?>.*?</\1>")
|
|
70
|
+
_HEAD_BLOCK_RE = re.compile(r"(?is)<head\b.*?</head>")
|
|
71
|
+
_TITLE_RE = re.compile(r"(?is)<title[^>]*>(.*?)</title>")
|
|
72
|
+
_TAG_RE = re.compile(r"(?is)<[^>]+>")
|
|
73
|
+
_BREAK_TAG_RE = re.compile(r"(?is)<br\s*/?>")
|
|
74
|
+
_BLOCK_CLOSE_TAG_RE = re.compile(
|
|
75
|
+
r"(?is)</(p|div|section|article|header|footer|aside|nav|h[1-6]|li|tr|td|th|"
|
|
76
|
+
r"table|blockquote|ul|ol)>"
|
|
77
|
+
)
|
|
78
|
+
_HTML_HINT_RE = re.compile(r"(?is)<!doctype\s+html|<html\b|<body\b|<head\b|<title\b")
|
|
79
|
+
_CHARSET_RE = re.compile(r"charset=([^\s;]+)", re.IGNORECASE)
|
|
80
|
+
_REMOTE_CHALLENGE_BODY_RE = re.compile(
|
|
81
|
+
r"(?is)\b(cloudflare|checking your browser|just a moment|enable javascript and cookies)\b"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
ResolverFn = Callable[[str, int], list[str]]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _normalize_whitespace(text: str) -> str:
|
|
89
|
+
return " ".join(str(text).split())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _normalize_content_type(raw: str) -> str:
|
|
93
|
+
text = str(raw or "").strip()
|
|
94
|
+
if not text:
|
|
95
|
+
return ""
|
|
96
|
+
return text.split(";", 1)[0].strip().lower()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _extract_charset(raw_content_type: str) -> str | None:
|
|
100
|
+
match = _CHARSET_RE.search(str(raw_content_type or ""))
|
|
101
|
+
if match is None:
|
|
102
|
+
return None
|
|
103
|
+
value = str(match.group(1) or "").strip().strip('"').strip("'")
|
|
104
|
+
return value or None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _decode_bytes(data: bytes, *, raw_content_type: str, fallback_encoding: str | None) -> str:
|
|
108
|
+
encoding = _extract_charset(raw_content_type) or (fallback_encoding or "").strip() or "utf-8"
|
|
109
|
+
try:
|
|
110
|
+
return data.decode(encoding, errors="replace")
|
|
111
|
+
except LookupError:
|
|
112
|
+
return data.decode("utf-8", errors="replace")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _looks_binary(data: bytes) -> bool:
|
|
116
|
+
if not data:
|
|
117
|
+
return False
|
|
118
|
+
if b"\x00" in data:
|
|
119
|
+
return True
|
|
120
|
+
sample = data[:2048]
|
|
121
|
+
text_like = set(range(32, 127)) | {9, 10, 13}
|
|
122
|
+
non_text = sum(1 for byte in sample if byte not in text_like)
|
|
123
|
+
return (non_text / max(1, len(sample))) > 0.30
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _looks_like_html(text: str) -> bool:
|
|
127
|
+
return _HTML_HINT_RE.search(text) is not None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _extract_html_title_and_text(html_text: str) -> tuple[str, str]:
|
|
131
|
+
title = ""
|
|
132
|
+
title_match = _TITLE_RE.search(html_text)
|
|
133
|
+
if title_match is not None:
|
|
134
|
+
raw_title = _TAG_RE.sub(" ", str(title_match.group(1) or ""))
|
|
135
|
+
title = _normalize_whitespace(html.unescape(raw_title))
|
|
136
|
+
|
|
137
|
+
body = _TAG_BLOCK_RE.sub(" ", html_text)
|
|
138
|
+
body = _HEAD_BLOCK_RE.sub(" ", body)
|
|
139
|
+
body = _BREAK_TAG_RE.sub("\n", body)
|
|
140
|
+
body = _BLOCK_CLOSE_TAG_RE.sub("\n", body)
|
|
141
|
+
body = _TAG_RE.sub(" ", body)
|
|
142
|
+
body = html.unescape(body)
|
|
143
|
+
|
|
144
|
+
lines = [_normalize_whitespace(line) for line in body.splitlines()]
|
|
145
|
+
text = "\n".join(line for line in lines if line)
|
|
146
|
+
if not text:
|
|
147
|
+
text = _normalize_whitespace(body)
|
|
148
|
+
return title, text
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _is_json_content_type(content_type: str) -> bool:
|
|
152
|
+
return content_type in _JSON_CONTENT_TYPES or content_type.endswith("+json")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _is_xml_content_type(content_type: str) -> bool:
|
|
156
|
+
return content_type in _XML_CONTENT_TYPES or content_type.endswith("+xml")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_text_like_content_type(content_type: str) -> bool:
|
|
160
|
+
if not content_type:
|
|
161
|
+
return True
|
|
162
|
+
if content_type.startswith("text/"):
|
|
163
|
+
return True
|
|
164
|
+
if _is_json_content_type(content_type):
|
|
165
|
+
return True
|
|
166
|
+
if _is_xml_content_type(content_type):
|
|
167
|
+
return True
|
|
168
|
+
return content_type in _EXTRA_TEXT_CONTENT_TYPES
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _clip_text(text: str, *, max_chars: int) -> tuple[str, bool]:
|
|
172
|
+
if len(text) <= max_chars:
|
|
173
|
+
return text, False
|
|
174
|
+
return text[:max_chars], True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _coerce_max_chars(raw: Any) -> int:
|
|
178
|
+
try:
|
|
179
|
+
value = int(raw)
|
|
180
|
+
except (TypeError, ValueError) as e:
|
|
181
|
+
raise WebFetchError(
|
|
182
|
+
f"max_chars must be between 1 and {_MAX_MAX_CHARS}.",
|
|
183
|
+
recoverable=True,
|
|
184
|
+
) from e
|
|
185
|
+
if value <= 0 or value > _MAX_MAX_CHARS:
|
|
186
|
+
raise WebFetchError(
|
|
187
|
+
f"max_chars must be between 1 and {_MAX_MAX_CHARS}.",
|
|
188
|
+
recoverable=True,
|
|
189
|
+
)
|
|
190
|
+
return value
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _split_url(raw_url: str) -> tuple[str, Any]:
|
|
194
|
+
url = str(raw_url or "").strip()
|
|
195
|
+
if not url:
|
|
196
|
+
raise WebFetchError("url must be a non-empty string.", recoverable=True)
|
|
197
|
+
split = urlsplit(url)
|
|
198
|
+
scheme = split.scheme.lower()
|
|
199
|
+
if scheme not in {"http", "https"}:
|
|
200
|
+
raise WebFetchError(
|
|
201
|
+
f"Unsupported URL scheme: {split.scheme or '(missing)'}",
|
|
202
|
+
recoverable=True,
|
|
203
|
+
)
|
|
204
|
+
if split.hostname is None:
|
|
205
|
+
raise WebFetchError("URL must include a valid host.", recoverable=True)
|
|
206
|
+
if split.username is not None or split.password is not None:
|
|
207
|
+
raise WebFetchError("Embedded URL credentials are not allowed.", recoverable=True)
|
|
208
|
+
try:
|
|
209
|
+
_ = split.port
|
|
210
|
+
except ValueError as e:
|
|
211
|
+
raise WebFetchError("URL has an invalid port value.", recoverable=True) from e
|
|
212
|
+
return url, split
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _blocked_ip_reason(ip: ipaddress._BaseAddress) -> str | None:
|
|
216
|
+
if ip.is_loopback:
|
|
217
|
+
return "loopback"
|
|
218
|
+
if ip.is_private:
|
|
219
|
+
return "private"
|
|
220
|
+
if ip.is_link_local:
|
|
221
|
+
return "link-local"
|
|
222
|
+
if ip.is_multicast:
|
|
223
|
+
return "multicast"
|
|
224
|
+
if ip.is_unspecified:
|
|
225
|
+
return "unspecified"
|
|
226
|
+
if ip.is_reserved:
|
|
227
|
+
return "reserved"
|
|
228
|
+
if isinstance(ip, ipaddress.IPv4Address):
|
|
229
|
+
if any(ip in network for network in _BLOCKED_IPV4_NETWORKS):
|
|
230
|
+
return "cloud-metadata-or-special-range"
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _resolve_host_addresses(host: str, port: int) -> list[str]:
|
|
235
|
+
try:
|
|
236
|
+
infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
|
|
237
|
+
except OSError as e:
|
|
238
|
+
raise WebFetchError(f"Failed to resolve host '{host}': {e}") from e
|
|
239
|
+
|
|
240
|
+
addresses: list[str] = []
|
|
241
|
+
seen: set[str] = set()
|
|
242
|
+
for _family, _socktype, _proto, _canonname, sockaddr in infos:
|
|
243
|
+
if not sockaddr:
|
|
244
|
+
continue
|
|
245
|
+
ip_text = str(sockaddr[0] or "").strip()
|
|
246
|
+
if not ip_text:
|
|
247
|
+
continue
|
|
248
|
+
if ip_text not in seen:
|
|
249
|
+
seen.add(ip_text)
|
|
250
|
+
addresses.append(ip_text)
|
|
251
|
+
if not addresses:
|
|
252
|
+
raise WebFetchError(f"Host '{host}' did not resolve to any IP address.")
|
|
253
|
+
return addresses
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _validate_fetch_target(url: str, *, resolver: ResolverFn) -> None:
|
|
257
|
+
_url, split = _split_url(url)
|
|
258
|
+
host = str(split.hostname or "")
|
|
259
|
+
host_normalized = host.rstrip(".").casefold()
|
|
260
|
+
if host_normalized == "localhost" or host_normalized.endswith(".localhost"):
|
|
261
|
+
raise WebFetchError(f"Blocked URL host '{host}': localhost is not allowed.")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
literal_ip = ipaddress.ip_address(host)
|
|
265
|
+
except ValueError:
|
|
266
|
+
literal_ip = None
|
|
267
|
+
|
|
268
|
+
if literal_ip is not None:
|
|
269
|
+
reason = _blocked_ip_reason(literal_ip)
|
|
270
|
+
if reason is not None:
|
|
271
|
+
raise WebFetchError(f"Blocked URL host '{host}': {reason} address is not allowed.")
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
port = split.port
|
|
275
|
+
if port is None:
|
|
276
|
+
port = 443 if split.scheme.lower() == "https" else 80
|
|
277
|
+
|
|
278
|
+
resolved_ips = resolver(host, port)
|
|
279
|
+
blocked_entries: list[str] = []
|
|
280
|
+
for ip_text in resolved_ips:
|
|
281
|
+
try:
|
|
282
|
+
parsed = ipaddress.ip_address(ip_text)
|
|
283
|
+
except ValueError as e:
|
|
284
|
+
raise WebFetchError(
|
|
285
|
+
f"Resolver returned a non-IP address for host '{host}': {ip_text!r}"
|
|
286
|
+
) from e
|
|
287
|
+
reason = _blocked_ip_reason(parsed)
|
|
288
|
+
if reason is not None:
|
|
289
|
+
blocked_entries.append(f"{parsed.compressed} ({reason})")
|
|
290
|
+
|
|
291
|
+
if blocked_entries:
|
|
292
|
+
details = ", ".join(blocked_entries)
|
|
293
|
+
raise WebFetchError(
|
|
294
|
+
f"Blocked URL host '{host}': resolved to blocked/local address(es): {details}."
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _read_limited_response_bytes(response: httpx.Response, *, limit: int) -> tuple[bytes, bool]:
|
|
299
|
+
buf = bytearray()
|
|
300
|
+
truncated = False
|
|
301
|
+
for chunk in response.iter_bytes():
|
|
302
|
+
if not chunk:
|
|
303
|
+
continue
|
|
304
|
+
remaining = limit - len(buf)
|
|
305
|
+
if remaining <= 0:
|
|
306
|
+
truncated = True
|
|
307
|
+
break
|
|
308
|
+
if len(chunk) > remaining:
|
|
309
|
+
buf.extend(chunk[:remaining])
|
|
310
|
+
truncated = True
|
|
311
|
+
break
|
|
312
|
+
buf.extend(chunk)
|
|
313
|
+
return bytes(buf), truncated
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _http_status_error_message(
|
|
317
|
+
*,
|
|
318
|
+
status_code: int,
|
|
319
|
+
final_url: str,
|
|
320
|
+
response_headers: dict[str, str],
|
|
321
|
+
decoded_body: str,
|
|
322
|
+
) -> str:
|
|
323
|
+
if status_code in {403, 429, 503}:
|
|
324
|
+
cf_mitigated = str(response_headers.get("cf-mitigated") or "").strip().casefold()
|
|
325
|
+
server = str(response_headers.get("server") or "").strip().casefold()
|
|
326
|
+
body_sample = decoded_body[:4096]
|
|
327
|
+
if cf_mitigated == "challenge" or (
|
|
328
|
+
"cloudflare" in server and _REMOTE_CHALLENGE_BODY_RE.search(body_sample)
|
|
329
|
+
):
|
|
330
|
+
return (
|
|
331
|
+
f"HTTP error {status_code} while fetching '{final_url}': remote site "
|
|
332
|
+
"blocked automated retrieval with anti-bot/challenge protection."
|
|
333
|
+
)
|
|
334
|
+
return f"HTTP error {status_code} while fetching '{final_url}'."
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def web_fetch(
|
|
338
|
+
*,
|
|
339
|
+
url: str,
|
|
340
|
+
max_chars: int = _DEFAULT_MAX_CHARS,
|
|
341
|
+
transport: httpx.BaseTransport | None = None,
|
|
342
|
+
) -> dict[str, Any]:
|
|
343
|
+
# Defense-in-depth runtime guard: even if a web_fetch call reaches this layer
|
|
344
|
+
# through a path that bypassed tool registration (resume/replay, patched tool
|
|
345
|
+
# tables), the master web-tools switch still hard-blocks execution.
|
|
346
|
+
# Lazy import to avoid a module-level import cycle with config.
|
|
347
|
+
from ..config import resolve_web_tools_enabled
|
|
348
|
+
|
|
349
|
+
if not resolve_web_tools_enabled(None):
|
|
350
|
+
raise WebFetchError(
|
|
351
|
+
"web_fetch is disabled by the web-tools policy "
|
|
352
|
+
"(ALYSIS_WEB_TOOLS=off / web_tools_enabled=false). "
|
|
353
|
+
"No network fetch was performed.",
|
|
354
|
+
recoverable=False,
|
|
355
|
+
)
|
|
356
|
+
raw_input_url = str(url or "").strip()
|
|
357
|
+
requested_url, _split = _split_url(
|
|
358
|
+
normalize_web_url(raw_input_url)
|
|
359
|
+
or canonicalize_web_url_input(raw_input_url)
|
|
360
|
+
or raw_input_url
|
|
361
|
+
)
|
|
362
|
+
clipped_max_chars = _coerce_max_chars(max_chars)
|
|
363
|
+
|
|
364
|
+
resolver: ResolverFn = _resolve_host_addresses
|
|
365
|
+
timeout_budget = build_http_timeout_budget(_DEFAULT_TIMEOUT_SECONDS, profile="fetch")
|
|
366
|
+
|
|
367
|
+
current_url = requested_url
|
|
368
|
+
final_url = requested_url
|
|
369
|
+
status_code = 0
|
|
370
|
+
raw_content_type = ""
|
|
371
|
+
body = b""
|
|
372
|
+
body_truncated = False
|
|
373
|
+
response_encoding: str | None = None
|
|
374
|
+
response_headers: dict[str, str] = {}
|
|
375
|
+
|
|
376
|
+
_validate_fetch_target(current_url, resolver=resolver)
|
|
377
|
+
try:
|
|
378
|
+
response = asyncio.run(
|
|
379
|
+
safe_http_request(
|
|
380
|
+
"GET",
|
|
381
|
+
current_url,
|
|
382
|
+
timeout=_DEFAULT_TIMEOUT_SECONDS,
|
|
383
|
+
max_bytes=_MAX_RESPONSE_BYTES,
|
|
384
|
+
allow_redirects=True,
|
|
385
|
+
max_redirects=_MAX_REDIRECTS,
|
|
386
|
+
headers=_TEXT_HEADERS,
|
|
387
|
+
_transport=transport, # type: ignore[arg-type]
|
|
388
|
+
_resolver=resolver,
|
|
389
|
+
)
|
|
390
|
+
)
|
|
391
|
+
except SafeHttpError as e:
|
|
392
|
+
# safe_http classifies URL-/host-specific failures (oversized body,
|
|
393
|
+
# redirect problems, blocked or invalid hosts) as recoverable; propagate
|
|
394
|
+
# that so one bad target does not disable web tools for the whole turn.
|
|
395
|
+
raise WebFetchError(str(e), recoverable=getattr(e, "recoverable", False)) from e
|
|
396
|
+
except httpx.TimeoutException as e:
|
|
397
|
+
raise WebFetchError(
|
|
398
|
+
format_http_timeout_error(
|
|
399
|
+
operation=f"web_fetch request to '{current_url}'",
|
|
400
|
+
budget=timeout_budget,
|
|
401
|
+
error=e,
|
|
402
|
+
)
|
|
403
|
+
) from e
|
|
404
|
+
except httpx.HTTPError as e:
|
|
405
|
+
raise WebFetchError(f"HTTP request failed for '{current_url}': {e}") from e
|
|
406
|
+
|
|
407
|
+
status_code = int(response.status_code)
|
|
408
|
+
final_url = str(response.url)
|
|
409
|
+
raw_content_type = str(response.headers.get("content-type") or "")
|
|
410
|
+
response_encoding = response.encoding
|
|
411
|
+
response_headers = {str(key).lower(): str(value) for key, value in response.headers.items()}
|
|
412
|
+
body = response.content
|
|
413
|
+
body_truncated = False
|
|
414
|
+
|
|
415
|
+
content_type = _normalize_content_type(raw_content_type)
|
|
416
|
+
decoded = _decode_bytes(
|
|
417
|
+
body, raw_content_type=raw_content_type, fallback_encoding=response_encoding
|
|
418
|
+
)
|
|
419
|
+
# The site responded, so the web itself is reachable: an HTTP error status or
|
|
420
|
+
# unusable payload is specific to this URL and the model can recover by
|
|
421
|
+
# choosing a different source. Only genuine connectivity signals (DNS
|
|
422
|
+
# resolution outage, connect failures, timeouts) remain unrecoverable.
|
|
423
|
+
if status_code >= 400:
|
|
424
|
+
raise WebFetchError(
|
|
425
|
+
_http_status_error_message(
|
|
426
|
+
status_code=status_code,
|
|
427
|
+
final_url=final_url,
|
|
428
|
+
response_headers=response_headers,
|
|
429
|
+
decoded_body=decoded,
|
|
430
|
+
),
|
|
431
|
+
recoverable=True,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
if content_type and not _is_text_like_content_type(content_type):
|
|
435
|
+
raise WebFetchError(
|
|
436
|
+
f"Unsupported content type for web_fetch: '{content_type}'. Only text-like responses "
|
|
437
|
+
"are supported.",
|
|
438
|
+
recoverable=True,
|
|
439
|
+
)
|
|
440
|
+
if not content_type and _looks_binary(body):
|
|
441
|
+
raise WebFetchError(
|
|
442
|
+
"Unsupported binary response without a text-like content type.",
|
|
443
|
+
recoverable=True,
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
title = ""
|
|
447
|
+
if content_type in _HTML_CONTENT_TYPES or (not content_type and _looks_like_html(decoded)):
|
|
448
|
+
if not content_type:
|
|
449
|
+
content_type = "text/html"
|
|
450
|
+
title, extracted = _extract_html_title_and_text(decoded)
|
|
451
|
+
readable_content = extracted
|
|
452
|
+
elif _is_json_content_type(content_type):
|
|
453
|
+
try:
|
|
454
|
+
parsed_json = json.loads(decoded)
|
|
455
|
+
except json.JSONDecodeError:
|
|
456
|
+
readable_content = decoded
|
|
457
|
+
else:
|
|
458
|
+
readable_content = json.dumps(parsed_json, ensure_ascii=False, indent=2)
|
|
459
|
+
else:
|
|
460
|
+
if not content_type:
|
|
461
|
+
content_type = "text/plain"
|
|
462
|
+
readable_content = decoded
|
|
463
|
+
|
|
464
|
+
readable_content = readable_content.replace("\r\n", "\n").replace("\r", "\n")
|
|
465
|
+
clipped_content, clipped = _clip_text(readable_content, max_chars=clipped_max_chars)
|
|
466
|
+
truncated = bool(body_truncated or clipped)
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
"url": requested_url,
|
|
470
|
+
"final_url": final_url,
|
|
471
|
+
"status_code": status_code,
|
|
472
|
+
"content_type": content_type,
|
|
473
|
+
"title": title,
|
|
474
|
+
"content": clipped_content,
|
|
475
|
+
"truncated": truncated,
|
|
476
|
+
"backend": "httpx",
|
|
477
|
+
**(
|
|
478
|
+
{"raw_input_url": raw_input_url}
|
|
479
|
+
if raw_input_url and raw_input_url != requested_url
|
|
480
|
+
else {}
|
|
481
|
+
),
|
|
482
|
+
}
|