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,329 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..runtime_kind import RuntimeKind, normalize_runtime_kind
|
|
7
|
+
from .errors import McpProtocolError
|
|
8
|
+
from .models import PromptsMode
|
|
9
|
+
from .untrusted_content import build_untrusted_mcp_text_block
|
|
10
|
+
|
|
11
|
+
_PROMPT_ENABLED_RUNTIME_KINDS = frozenset(
|
|
12
|
+
{
|
|
13
|
+
RuntimeKind.INTERACTIVE_CHAT,
|
|
14
|
+
RuntimeKind.ONE_SHOT,
|
|
15
|
+
RuntimeKind.FORGE_EXEC,
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
_INLINE_PROMPT_TEXT_CHAR_LIMIT = 12_000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class McpPromptNormalizationError(ValueError, McpProtocolError):
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
message: str = "",
|
|
25
|
+
*,
|
|
26
|
+
server_id: str | None = None,
|
|
27
|
+
method: str | None = None,
|
|
28
|
+
cause: BaseException | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.server_id = server_id
|
|
32
|
+
self.method = method
|
|
33
|
+
self.cause = cause
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class McpPromptArgument:
|
|
38
|
+
name: str
|
|
39
|
+
description: str | None
|
|
40
|
+
required: bool
|
|
41
|
+
raw_payload: dict[str, Any] = field(repr=False)
|
|
42
|
+
|
|
43
|
+
def as_payload(self) -> dict[str, Any]:
|
|
44
|
+
payload: dict[str, Any] = {
|
|
45
|
+
"name": self.name,
|
|
46
|
+
"required": self.required,
|
|
47
|
+
}
|
|
48
|
+
if self.description:
|
|
49
|
+
payload["description"] = self.description
|
|
50
|
+
return payload
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class McpListedPrompt:
|
|
55
|
+
name: str
|
|
56
|
+
title: str | None
|
|
57
|
+
description: str | None
|
|
58
|
+
arguments: tuple[McpPromptArgument, ...]
|
|
59
|
+
raw_payload: dict[str, Any] = field(repr=False)
|
|
60
|
+
|
|
61
|
+
def as_payload(self, *, server_id: str) -> dict[str, Any]:
|
|
62
|
+
payload: dict[str, Any] = {
|
|
63
|
+
"server_id": server_id,
|
|
64
|
+
"name": self.name,
|
|
65
|
+
"arguments": [argument.as_payload() for argument in self.arguments],
|
|
66
|
+
}
|
|
67
|
+
if self.title:
|
|
68
|
+
payload["title"] = self.title
|
|
69
|
+
if self.description:
|
|
70
|
+
payload["description"] = self.description
|
|
71
|
+
return payload
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class McpPromptMessage:
|
|
76
|
+
role: str
|
|
77
|
+
content: tuple[dict[str, Any], ...]
|
|
78
|
+
content_summary: str
|
|
79
|
+
text: str | None = None
|
|
80
|
+
raw_payload: dict[str, Any] = field(repr=False, default_factory=dict)
|
|
81
|
+
|
|
82
|
+
def as_payload(self, *, server_id: str, prompt_name: str) -> dict[str, Any]:
|
|
83
|
+
content_items: list[dict[str, Any]] = []
|
|
84
|
+
for item in self.content:
|
|
85
|
+
payload_item = dict(item)
|
|
86
|
+
item_text = payload_item.get("text")
|
|
87
|
+
mime_type = payload_item.get("mime_type")
|
|
88
|
+
if isinstance(item_text, str):
|
|
89
|
+
payload_item["text"] = build_untrusted_mcp_text_block(
|
|
90
|
+
source_type="prompt_get",
|
|
91
|
+
server_id=server_id,
|
|
92
|
+
source_name=prompt_name,
|
|
93
|
+
text=item_text,
|
|
94
|
+
mime_type=mime_type if isinstance(mime_type, str) else None,
|
|
95
|
+
)
|
|
96
|
+
content_items.append(payload_item)
|
|
97
|
+
payload: dict[str, Any] = {
|
|
98
|
+
"role": self.role,
|
|
99
|
+
"content_summary": self.content_summary,
|
|
100
|
+
"content": content_items,
|
|
101
|
+
}
|
|
102
|
+
if self.text is not None:
|
|
103
|
+
payload["text"] = build_untrusted_mcp_text_block(
|
|
104
|
+
source_type="prompt_get",
|
|
105
|
+
server_id=server_id,
|
|
106
|
+
source_name=prompt_name,
|
|
107
|
+
text=self.text,
|
|
108
|
+
)
|
|
109
|
+
return payload
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class McpGetPromptResult:
|
|
114
|
+
description: str | None
|
|
115
|
+
messages: tuple[McpPromptMessage, ...]
|
|
116
|
+
content_summary: str
|
|
117
|
+
text: str
|
|
118
|
+
raw_payload: dict[str, Any] = field(repr=False)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def prompts_runtime_supported(runtime_kind: RuntimeKind | str) -> bool:
|
|
122
|
+
return normalize_runtime_kind(runtime_kind) in _PROMPT_ENABLED_RUNTIME_KINDS
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def prompts_mode_enabled(
|
|
126
|
+
*,
|
|
127
|
+
prompts_mode: PromptsMode,
|
|
128
|
+
runtime_kind: RuntimeKind | str,
|
|
129
|
+
) -> bool:
|
|
130
|
+
return prompts_mode == "listed_get_only" and prompts_runtime_supported(runtime_kind)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _require_object(value: Any, *, context: str) -> dict[str, Any]:
|
|
134
|
+
if not isinstance(value, dict):
|
|
135
|
+
raise McpPromptNormalizationError(f"{context} must be an object.")
|
|
136
|
+
return value
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _require_string(value: Any, *, context: str) -> str:
|
|
140
|
+
if not isinstance(value, str):
|
|
141
|
+
raise McpPromptNormalizationError(f"{context} must be a string.")
|
|
142
|
+
cleaned = value.strip()
|
|
143
|
+
if not cleaned:
|
|
144
|
+
raise McpPromptNormalizationError(f"{context} cannot be empty.")
|
|
145
|
+
return cleaned
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _optional_string(value: Any, *, context: str) -> str | None:
|
|
149
|
+
if value is None:
|
|
150
|
+
return None
|
|
151
|
+
if not isinstance(value, str):
|
|
152
|
+
raise McpPromptNormalizationError(f"{context} must be a string when present.")
|
|
153
|
+
cleaned = value.strip()
|
|
154
|
+
return cleaned or None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _normalize_prompt_arguments(value: Any, *, context: str) -> tuple[McpPromptArgument, ...]:
|
|
158
|
+
if value is None:
|
|
159
|
+
return ()
|
|
160
|
+
if not isinstance(value, list):
|
|
161
|
+
raise McpPromptNormalizationError(f"{context} must be an array when present.")
|
|
162
|
+
normalized: list[McpPromptArgument] = []
|
|
163
|
+
seen_names: set[str] = set()
|
|
164
|
+
for index, raw_item in enumerate(value):
|
|
165
|
+
payload = _require_object(raw_item, context=f"{context}[{index}]")
|
|
166
|
+
name = _require_string(payload.get("name"), context=f"{context}[{index}].name")
|
|
167
|
+
name_key = name.casefold()
|
|
168
|
+
if name_key in seen_names:
|
|
169
|
+
raise McpPromptNormalizationError(
|
|
170
|
+
f"{context} contains repeated argument name {name!r}."
|
|
171
|
+
)
|
|
172
|
+
seen_names.add(name_key)
|
|
173
|
+
required = payload.get("required", False)
|
|
174
|
+
if not isinstance(required, bool):
|
|
175
|
+
raise McpPromptNormalizationError(
|
|
176
|
+
f"{context}[{index}].required must be a boolean when present."
|
|
177
|
+
)
|
|
178
|
+
normalized.append(
|
|
179
|
+
McpPromptArgument(
|
|
180
|
+
name=name,
|
|
181
|
+
description=_optional_string(
|
|
182
|
+
payload.get("description"),
|
|
183
|
+
context=f"{context}[{index}].description",
|
|
184
|
+
),
|
|
185
|
+
required=required,
|
|
186
|
+
raw_payload=dict(payload),
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
return tuple(normalized)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _normalize_prompt_content_items(
|
|
193
|
+
value: Any, *, context: str
|
|
194
|
+
) -> tuple[tuple[dict[str, Any], ...], str]:
|
|
195
|
+
raw_items = value if isinstance(value, list) else [value]
|
|
196
|
+
normalized: list[dict[str, Any]] = []
|
|
197
|
+
summaries: list[str] = []
|
|
198
|
+
for index, raw_item in enumerate(raw_items):
|
|
199
|
+
payload = _require_object(raw_item, context=f"{context}[{index}]")
|
|
200
|
+
item_type = _require_string(payload.get("type"), context=f"{context}[{index}].type")
|
|
201
|
+
normalized_item: dict[str, Any] = {"type": item_type}
|
|
202
|
+
summary = f"{item_type} item omitted"
|
|
203
|
+
mime_type = _optional_string(
|
|
204
|
+
payload.get("mimeType"),
|
|
205
|
+
context=f"{context}[{index}].mimeType",
|
|
206
|
+
)
|
|
207
|
+
if mime_type:
|
|
208
|
+
normalized_item["mime_type"] = mime_type
|
|
209
|
+
summary = f"{mime_type} {summary}"
|
|
210
|
+
if item_type == "text":
|
|
211
|
+
text = _require_string(payload.get("text"), context=f"{context}[{index}].text")
|
|
212
|
+
inline_text = text
|
|
213
|
+
truncated = False
|
|
214
|
+
if len(inline_text) > _INLINE_PROMPT_TEXT_CHAR_LIMIT:
|
|
215
|
+
inline_text = inline_text[:_INLINE_PROMPT_TEXT_CHAR_LIMIT]
|
|
216
|
+
truncated = True
|
|
217
|
+
summary = f"text({len(text)} chars)"
|
|
218
|
+
if mime_type:
|
|
219
|
+
summary = f"{mime_type} {summary}"
|
|
220
|
+
if truncated:
|
|
221
|
+
summary += f", truncated to {_INLINE_PROMPT_TEXT_CHAR_LIMIT} chars"
|
|
222
|
+
normalized_item["text"] = inline_text
|
|
223
|
+
else:
|
|
224
|
+
uri = _optional_string(payload.get("uri"), context=f"{context}[{index}].uri")
|
|
225
|
+
if uri:
|
|
226
|
+
normalized_item["uri"] = uri
|
|
227
|
+
resource = payload.get("resource")
|
|
228
|
+
if isinstance(resource, dict):
|
|
229
|
+
resource_uri = _optional_string(
|
|
230
|
+
resource.get("uri"),
|
|
231
|
+
context=f"{context}[{index}].resource.uri",
|
|
232
|
+
)
|
|
233
|
+
if resource_uri:
|
|
234
|
+
normalized_item["resource_uri"] = resource_uri
|
|
235
|
+
normalized_item["summary"] = summary
|
|
236
|
+
normalized.append(normalized_item)
|
|
237
|
+
summaries.append(summary)
|
|
238
|
+
return tuple(normalized), ", ".join(summaries) or "no content"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def normalize_list_prompts_result(
|
|
242
|
+
result: dict[str, Any],
|
|
243
|
+
) -> tuple[tuple[McpListedPrompt, ...], str | None]:
|
|
244
|
+
prompts = result.get("prompts")
|
|
245
|
+
if not isinstance(prompts, list):
|
|
246
|
+
raise McpPromptNormalizationError("prompts/list result.prompts must be an array.")
|
|
247
|
+
normalized: list[McpListedPrompt] = []
|
|
248
|
+
for index, raw_item in enumerate(prompts):
|
|
249
|
+
payload = _require_object(raw_item, context=f"prompts/list prompt[{index}]")
|
|
250
|
+
normalized.append(
|
|
251
|
+
McpListedPrompt(
|
|
252
|
+
name=_require_string(
|
|
253
|
+
payload.get("name"), context=f"prompts/list prompt[{index}].name"
|
|
254
|
+
),
|
|
255
|
+
title=_optional_string(
|
|
256
|
+
payload.get("title"),
|
|
257
|
+
context=f"prompts/list prompt[{index}].title",
|
|
258
|
+
),
|
|
259
|
+
description=_optional_string(
|
|
260
|
+
payload.get("description"),
|
|
261
|
+
context=f"prompts/list prompt[{index}].description",
|
|
262
|
+
),
|
|
263
|
+
arguments=_normalize_prompt_arguments(
|
|
264
|
+
payload.get("arguments"),
|
|
265
|
+
context=f"prompts/list prompt[{index}].arguments",
|
|
266
|
+
),
|
|
267
|
+
raw_payload=dict(payload),
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
next_cursor = result.get("nextCursor")
|
|
271
|
+
if next_cursor is None:
|
|
272
|
+
return tuple(normalized), None
|
|
273
|
+
return tuple(normalized), _require_string(next_cursor, context="prompts/list nextCursor")
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def normalize_get_prompt_result(
|
|
277
|
+
result: dict[str, Any],
|
|
278
|
+
*,
|
|
279
|
+
expected_name: str,
|
|
280
|
+
) -> McpGetPromptResult:
|
|
281
|
+
if "name" in result:
|
|
282
|
+
result_name = _require_string(result.get("name"), context="prompts/get result.name")
|
|
283
|
+
if result_name != expected_name:
|
|
284
|
+
raise McpPromptNormalizationError(
|
|
285
|
+
"prompts/get result.name must match the requested prompt name."
|
|
286
|
+
)
|
|
287
|
+
messages = result.get("messages")
|
|
288
|
+
if not isinstance(messages, list):
|
|
289
|
+
raise McpPromptNormalizationError("prompts/get result.messages must be an array.")
|
|
290
|
+
normalized_messages: list[McpPromptMessage] = []
|
|
291
|
+
text_parts: list[str] = []
|
|
292
|
+
summaries: list[str] = []
|
|
293
|
+
for index, raw_item in enumerate(messages):
|
|
294
|
+
payload = _require_object(raw_item, context=f"prompts/get messages[{index}]")
|
|
295
|
+
role = _require_string(payload.get("role"), context=f"prompts/get messages[{index}].role")
|
|
296
|
+
content_items, content_summary = _normalize_prompt_content_items(
|
|
297
|
+
payload.get("content"),
|
|
298
|
+
context=f"prompts/get messages[{index}].content",
|
|
299
|
+
)
|
|
300
|
+
text_parts.extend(
|
|
301
|
+
item["text"]
|
|
302
|
+
for item in content_items
|
|
303
|
+
if isinstance(item.get("text"), str) and str(item.get("text")).strip()
|
|
304
|
+
)
|
|
305
|
+
summaries.append(f"{role}: {content_summary}")
|
|
306
|
+
normalized_messages.append(
|
|
307
|
+
McpPromptMessage(
|
|
308
|
+
role=role,
|
|
309
|
+
content=content_items,
|
|
310
|
+
content_summary=content_summary,
|
|
311
|
+
text="\n\n".join(
|
|
312
|
+
item["text"]
|
|
313
|
+
for item in content_items
|
|
314
|
+
if isinstance(item.get("text"), str) and str(item.get("text")).strip()
|
|
315
|
+
)
|
|
316
|
+
or None,
|
|
317
|
+
raw_payload=dict(payload),
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
return McpGetPromptResult(
|
|
321
|
+
description=_optional_string(
|
|
322
|
+
result.get("description"),
|
|
323
|
+
context="prompts/get result.description",
|
|
324
|
+
),
|
|
325
|
+
messages=tuple(normalized_messages),
|
|
326
|
+
content_summary="; ".join(summaries) or "no messages",
|
|
327
|
+
text="\n\n".join(text_parts),
|
|
328
|
+
raw_payload=dict(result),
|
|
329
|
+
)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from ..runtime_kind import RuntimeKind, normalize_runtime_kind
|
|
7
|
+
from .errors import McpProtocolError
|
|
8
|
+
from .models import ResourcesMode
|
|
9
|
+
from .untrusted_content import _looks_like_binary_blob, build_untrusted_mcp_text_block
|
|
10
|
+
|
|
11
|
+
_RESOURCE_ENABLED_RUNTIME_KINDS = frozenset(
|
|
12
|
+
{
|
|
13
|
+
RuntimeKind.INTERACTIVE_CHAT,
|
|
14
|
+
RuntimeKind.ONE_SHOT,
|
|
15
|
+
RuntimeKind.FORGE_EXEC,
|
|
16
|
+
}
|
|
17
|
+
)
|
|
18
|
+
_INLINE_RESOURCE_TEXT_CHAR_LIMIT = 12_000
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class McpResourceNormalizationError(ValueError, McpProtocolError):
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
message: str = "",
|
|
25
|
+
*,
|
|
26
|
+
server_id: str | None = None,
|
|
27
|
+
method: str | None = None,
|
|
28
|
+
cause: BaseException | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.server_id = server_id
|
|
32
|
+
self.method = method
|
|
33
|
+
self.cause = cause
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class McpListedResource:
|
|
38
|
+
uri: str
|
|
39
|
+
name: str
|
|
40
|
+
description: str | None
|
|
41
|
+
mime_type: str | None
|
|
42
|
+
size: int | None
|
|
43
|
+
raw_payload: dict[str, Any] = field(repr=False)
|
|
44
|
+
|
|
45
|
+
def as_tool_payload(self, *, server_id: str) -> dict[str, Any]:
|
|
46
|
+
payload: dict[str, Any] = {
|
|
47
|
+
"server_id": server_id,
|
|
48
|
+
"uri": self.uri,
|
|
49
|
+
"name": self.name,
|
|
50
|
+
}
|
|
51
|
+
if self.description:
|
|
52
|
+
payload["description"] = self.description
|
|
53
|
+
if self.mime_type:
|
|
54
|
+
payload["mime_type"] = self.mime_type
|
|
55
|
+
if self.size is not None:
|
|
56
|
+
payload["size"] = self.size
|
|
57
|
+
return payload
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class McpReadResourceContent:
|
|
62
|
+
uri: str
|
|
63
|
+
mime_type: str | None
|
|
64
|
+
content_summary: str
|
|
65
|
+
text: str | None = None
|
|
66
|
+
raw_payload: dict[str, Any] = field(repr=False, default_factory=dict)
|
|
67
|
+
|
|
68
|
+
def as_tool_payload(self, *, server_id: str, resource_uri: str) -> dict[str, Any]:
|
|
69
|
+
payload: dict[str, Any] = {
|
|
70
|
+
"uri": self.uri,
|
|
71
|
+
"content_summary": self.content_summary,
|
|
72
|
+
}
|
|
73
|
+
if self.mime_type:
|
|
74
|
+
payload["mime_type"] = self.mime_type
|
|
75
|
+
if self.text is not None:
|
|
76
|
+
payload["text"] = build_untrusted_mcp_text_block(
|
|
77
|
+
source_type="resource_read",
|
|
78
|
+
server_id=server_id,
|
|
79
|
+
source_name=resource_uri,
|
|
80
|
+
text=self.text,
|
|
81
|
+
mime_type=self.mime_type,
|
|
82
|
+
)
|
|
83
|
+
return payload
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class McpReadResourceResult:
|
|
88
|
+
contents: tuple[McpReadResourceContent, ...]
|
|
89
|
+
content_summary: str
|
|
90
|
+
text: str
|
|
91
|
+
mime_type: str | None
|
|
92
|
+
raw_payload: dict[str, Any] = field(repr=False)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def resources_runtime_supported(runtime_kind: RuntimeKind | str) -> bool:
|
|
96
|
+
return normalize_runtime_kind(runtime_kind) in _RESOURCE_ENABLED_RUNTIME_KINDS
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def resources_mode_enabled(
|
|
100
|
+
*,
|
|
101
|
+
resources_mode: ResourcesMode,
|
|
102
|
+
runtime_kind: RuntimeKind | str,
|
|
103
|
+
) -> bool:
|
|
104
|
+
return resources_mode == "listed_read_only" and resources_runtime_supported(runtime_kind)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _require_object(value: Any, *, context: str) -> dict[str, Any]:
|
|
108
|
+
if not isinstance(value, dict):
|
|
109
|
+
raise McpResourceNormalizationError(f"{context} must be an object.")
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _require_string(value: Any, *, context: str) -> str:
|
|
114
|
+
if not isinstance(value, str):
|
|
115
|
+
raise McpResourceNormalizationError(f"{context} must be a string.")
|
|
116
|
+
cleaned = value.strip()
|
|
117
|
+
if not cleaned:
|
|
118
|
+
raise McpResourceNormalizationError(f"{context} cannot be empty.")
|
|
119
|
+
return cleaned
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _optional_string(value: Any, *, context: str) -> str | None:
|
|
123
|
+
if value is None:
|
|
124
|
+
return None
|
|
125
|
+
if not isinstance(value, str):
|
|
126
|
+
raise McpResourceNormalizationError(f"{context} must be a string when present.")
|
|
127
|
+
cleaned = value.strip()
|
|
128
|
+
return cleaned or None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _optional_size(value: Any, *, context: str) -> int | None:
|
|
132
|
+
if value is None:
|
|
133
|
+
return None
|
|
134
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
135
|
+
raise McpResourceNormalizationError(
|
|
136
|
+
f"{context} must be a non-negative integer when present."
|
|
137
|
+
)
|
|
138
|
+
if value < 0:
|
|
139
|
+
raise McpResourceNormalizationError(f"{context} must be >= 0 when present.")
|
|
140
|
+
return int(value)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _mime_is_textual(mime_type: str | None) -> bool:
|
|
144
|
+
if mime_type is None:
|
|
145
|
+
return True
|
|
146
|
+
lowered = mime_type.strip().lower()
|
|
147
|
+
if not lowered:
|
|
148
|
+
return True
|
|
149
|
+
if lowered.startswith("text/"):
|
|
150
|
+
return True
|
|
151
|
+
return (
|
|
152
|
+
lowered == "application/json"
|
|
153
|
+
or lowered.endswith("+json")
|
|
154
|
+
or lowered == "application/xml"
|
|
155
|
+
or lowered.endswith("+xml")
|
|
156
|
+
or lowered in {"application/yaml", "application/x-yaml", "text/yaml"}
|
|
157
|
+
or lowered.endswith("+yaml")
|
|
158
|
+
or lowered in {"application/javascript", "application/x-javascript"}
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _inline_text_payload(
|
|
163
|
+
*,
|
|
164
|
+
text: str,
|
|
165
|
+
mime_type: str | None,
|
|
166
|
+
) -> tuple[str | None, str]:
|
|
167
|
+
if not _mime_is_textual(mime_type):
|
|
168
|
+
mime_label = mime_type or "non-text"
|
|
169
|
+
return None, f"{mime_label} text omitted ({len(text)} chars)"
|
|
170
|
+
if _looks_like_binary_blob(text):
|
|
171
|
+
return None, f"binary-like text omitted ({len(text)} chars)"
|
|
172
|
+
inline_text = text
|
|
173
|
+
truncated = False
|
|
174
|
+
if len(inline_text) > _INLINE_RESOURCE_TEXT_CHAR_LIMIT:
|
|
175
|
+
inline_text = inline_text[:_INLINE_RESOURCE_TEXT_CHAR_LIMIT]
|
|
176
|
+
truncated = True
|
|
177
|
+
summary_kind = "json text" if mime_type and "json" in mime_type.lower() else "text"
|
|
178
|
+
summary = f"{summary_kind}({len(text)} chars)"
|
|
179
|
+
if mime_type:
|
|
180
|
+
summary = f"{mime_type} {summary}"
|
|
181
|
+
if truncated:
|
|
182
|
+
summary += f", truncated to {_INLINE_RESOURCE_TEXT_CHAR_LIMIT} chars"
|
|
183
|
+
return inline_text, summary
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def normalize_list_resources_result(
|
|
187
|
+
result: dict[str, Any],
|
|
188
|
+
) -> tuple[tuple[McpListedResource, ...], str | None]:
|
|
189
|
+
resources = result.get("resources")
|
|
190
|
+
if not isinstance(resources, list):
|
|
191
|
+
raise McpResourceNormalizationError("resources/list result.resources must be an array.")
|
|
192
|
+
normalized: list[McpListedResource] = []
|
|
193
|
+
for index, raw_item in enumerate(resources):
|
|
194
|
+
payload = _require_object(raw_item, context=f"resources/list resource[{index}]")
|
|
195
|
+
normalized.append(
|
|
196
|
+
McpListedResource(
|
|
197
|
+
uri=_require_string(
|
|
198
|
+
payload.get("uri"), context=f"resources/list resource[{index}].uri"
|
|
199
|
+
),
|
|
200
|
+
name=_require_string(
|
|
201
|
+
payload.get("name"), context=f"resources/list resource[{index}].name"
|
|
202
|
+
),
|
|
203
|
+
description=_optional_string(
|
|
204
|
+
payload.get("description"),
|
|
205
|
+
context=f"resources/list resource[{index}].description",
|
|
206
|
+
),
|
|
207
|
+
mime_type=_optional_string(
|
|
208
|
+
payload.get("mimeType"),
|
|
209
|
+
context=f"resources/list resource[{index}].mimeType",
|
|
210
|
+
),
|
|
211
|
+
size=_optional_size(
|
|
212
|
+
payload.get("size"),
|
|
213
|
+
context=f"resources/list resource[{index}].size",
|
|
214
|
+
),
|
|
215
|
+
raw_payload=dict(payload),
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
next_cursor = result.get("nextCursor")
|
|
219
|
+
if next_cursor is None:
|
|
220
|
+
return tuple(normalized), None
|
|
221
|
+
return tuple(normalized), _require_string(next_cursor, context="resources/list nextCursor")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def normalize_read_resource_result(
|
|
225
|
+
result: dict[str, Any],
|
|
226
|
+
*,
|
|
227
|
+
expected_uri: str,
|
|
228
|
+
) -> McpReadResourceResult:
|
|
229
|
+
contents = result.get("contents")
|
|
230
|
+
if not isinstance(contents, list):
|
|
231
|
+
raise McpResourceNormalizationError("resources/read result.contents must be an array.")
|
|
232
|
+
normalized: list[McpReadResourceContent] = []
|
|
233
|
+
text_parts: list[str] = []
|
|
234
|
+
for index, raw_item in enumerate(contents):
|
|
235
|
+
payload = _require_object(raw_item, context=f"resources/read contents[{index}]")
|
|
236
|
+
item_uri = _optional_string(
|
|
237
|
+
payload.get("uri"), context=f"resources/read contents[{index}].uri"
|
|
238
|
+
)
|
|
239
|
+
resolved_uri = item_uri or expected_uri
|
|
240
|
+
if resolved_uri != expected_uri:
|
|
241
|
+
raise McpResourceNormalizationError(
|
|
242
|
+
f"resources/read contents[{index}].uri must match the requested resource URI."
|
|
243
|
+
)
|
|
244
|
+
mime_type = _optional_string(
|
|
245
|
+
payload.get("mimeType"),
|
|
246
|
+
context=f"resources/read contents[{index}].mimeType",
|
|
247
|
+
)
|
|
248
|
+
has_text = "text" in payload
|
|
249
|
+
has_blob = "blob" in payload
|
|
250
|
+
if has_text and has_blob:
|
|
251
|
+
raise McpResourceNormalizationError(
|
|
252
|
+
f"resources/read contents[{index}] cannot include both text and blob."
|
|
253
|
+
)
|
|
254
|
+
if not has_text and not has_blob:
|
|
255
|
+
raise McpResourceNormalizationError(
|
|
256
|
+
f"resources/read contents[{index}] must include text or blob."
|
|
257
|
+
)
|
|
258
|
+
inline_text: str | None = None
|
|
259
|
+
content_summary: str
|
|
260
|
+
if has_text:
|
|
261
|
+
raw_text = payload.get("text")
|
|
262
|
+
if not isinstance(raw_text, str):
|
|
263
|
+
raise McpResourceNormalizationError(
|
|
264
|
+
f"resources/read contents[{index}].text must be a string."
|
|
265
|
+
)
|
|
266
|
+
inline_text, content_summary = _inline_text_payload(text=raw_text, mime_type=mime_type)
|
|
267
|
+
if inline_text is not None:
|
|
268
|
+
text_parts.append(inline_text)
|
|
269
|
+
else:
|
|
270
|
+
raw_blob = payload.get("blob")
|
|
271
|
+
if not isinstance(raw_blob, str):
|
|
272
|
+
raise McpResourceNormalizationError(
|
|
273
|
+
f"resources/read contents[{index}].blob must be a string."
|
|
274
|
+
)
|
|
275
|
+
blob_summary = f"blob omitted ({len(raw_blob)} chars)"
|
|
276
|
+
if mime_type:
|
|
277
|
+
blob_summary = f"{mime_type} {blob_summary}"
|
|
278
|
+
content_summary = blob_summary
|
|
279
|
+
normalized.append(
|
|
280
|
+
McpReadResourceContent(
|
|
281
|
+
uri=resolved_uri,
|
|
282
|
+
mime_type=mime_type,
|
|
283
|
+
content_summary=content_summary,
|
|
284
|
+
text=inline_text,
|
|
285
|
+
raw_payload=dict(payload),
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
mime_type = next((item.mime_type for item in normalized if item.mime_type), None)
|
|
289
|
+
return McpReadResourceResult(
|
|
290
|
+
contents=tuple(normalized),
|
|
291
|
+
content_summary=", ".join(item.content_summary for item in normalized) or "no content",
|
|
292
|
+
text="\n\n".join(text_parts),
|
|
293
|
+
mime_type=mime_type,
|
|
294
|
+
raw_payload=dict(result),
|
|
295
|
+
)
|