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,903 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import copy
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..branding import canonical_user_config_dir, env_get, env_get_from
|
|
14
|
+
from ..runtime_kind import RuntimeKind, runtime_kind_values
|
|
15
|
+
|
|
16
|
+
_DEFAULT_TIMEOUT_S = 15.0
|
|
17
|
+
_MAX_TIMEOUT_S = 300.0
|
|
18
|
+
_MAX_MANIFEST_BYTES = 32_000
|
|
19
|
+
_MAX_SCHEMA_BYTES = 24_000
|
|
20
|
+
_MAX_DESCRIPTION_CHARS = 600
|
|
21
|
+
_MAX_REQUIRED_ENV_VARS = 32
|
|
22
|
+
_MAX_SECRET_REFS = 32
|
|
23
|
+
_MAX_NETWORK_HOSTS = 64
|
|
24
|
+
_RESERVED_NAME_PREFIXES = ("mcp__",)
|
|
25
|
+
_RESERVED_EXACT_NAMES = frozenset({"mcp_resources_list", "mcp_resource_read"})
|
|
26
|
+
_VALID_ISOLATION_VALUES = {"subprocess"}
|
|
27
|
+
_SUPPORTED_MANIFEST_VERSION = 1
|
|
28
|
+
_VALID_NETWORK_ACCESS_VALUES = {
|
|
29
|
+
"unspecified",
|
|
30
|
+
"none",
|
|
31
|
+
"local",
|
|
32
|
+
"restricted",
|
|
33
|
+
"unrestricted",
|
|
34
|
+
}
|
|
35
|
+
_VALID_FILESYSTEM_SCOPE_VALUES = {
|
|
36
|
+
"unspecified",
|
|
37
|
+
"none",
|
|
38
|
+
"tool_dir",
|
|
39
|
+
"workspace",
|
|
40
|
+
"unrestricted",
|
|
41
|
+
}
|
|
42
|
+
_VALID_PROCESS_SPAWN_VALUES = {
|
|
43
|
+
"unspecified",
|
|
44
|
+
"none",
|
|
45
|
+
"unrestricted",
|
|
46
|
+
}
|
|
47
|
+
_DEFAULT_ENABLED_IN = (
|
|
48
|
+
RuntimeKind.INTERACTIVE_CHAT.value,
|
|
49
|
+
RuntimeKind.ONE_SHOT.value,
|
|
50
|
+
RuntimeKind.FORGE_EXEC.value,
|
|
51
|
+
)
|
|
52
|
+
_ENV_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class CustomToolCapabilities:
|
|
57
|
+
read_only: bool = False
|
|
58
|
+
destructive: bool = False
|
|
59
|
+
network_access: str = "unspecified"
|
|
60
|
+
network_hosts: tuple[str, ...] = ()
|
|
61
|
+
filesystem_read_scope: str = "unspecified"
|
|
62
|
+
filesystem_write_scope: str = "unspecified"
|
|
63
|
+
process_spawn: str = "unspecified"
|
|
64
|
+
secret_refs: tuple[str, ...] = ()
|
|
65
|
+
|
|
66
|
+
def to_dict(self) -> dict[str, Any]:
|
|
67
|
+
return {
|
|
68
|
+
"read_only": self.read_only,
|
|
69
|
+
"destructive": self.destructive,
|
|
70
|
+
"network_access": self.network_access,
|
|
71
|
+
"network_hosts": list(self.network_hosts),
|
|
72
|
+
"filesystem_read_scope": self.filesystem_read_scope,
|
|
73
|
+
"filesystem_write_scope": self.filesystem_write_scope,
|
|
74
|
+
"process_spawn": self.process_spawn,
|
|
75
|
+
"secret_refs": list(self.secret_refs),
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class CustomToolSpec:
|
|
81
|
+
name: str
|
|
82
|
+
description: str
|
|
83
|
+
input_schema: dict[str, Any]
|
|
84
|
+
manifest_version: int
|
|
85
|
+
capabilities: CustomToolCapabilities
|
|
86
|
+
output_schema: dict[str, Any] | None
|
|
87
|
+
timeout_s: float
|
|
88
|
+
required_env: tuple[str, ...]
|
|
89
|
+
enabled_in: tuple[str, ...]
|
|
90
|
+
isolation: str
|
|
91
|
+
source_scope: str
|
|
92
|
+
source_path: Path
|
|
93
|
+
relative_tool_path: str
|
|
94
|
+
file_hash: str
|
|
95
|
+
missing_env: tuple[str, ...]
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def normalized_name(self) -> str:
|
|
99
|
+
return self.name.casefold()
|
|
100
|
+
|
|
101
|
+
def metadata(self, *, include_output_schema: bool = False) -> dict[str, Any]:
|
|
102
|
+
payload: dict[str, Any] = {
|
|
103
|
+
"name": self.name,
|
|
104
|
+
"manifest_version": self.manifest_version,
|
|
105
|
+
"source_scope": self.source_scope,
|
|
106
|
+
"relative_tool_path": self.relative_tool_path,
|
|
107
|
+
"file_hash": self.file_hash,
|
|
108
|
+
"capabilities": self.capabilities.to_dict(),
|
|
109
|
+
"has_output_schema": self.output_schema is not None,
|
|
110
|
+
}
|
|
111
|
+
if include_output_schema and self.output_schema is not None:
|
|
112
|
+
payload["output_schema"] = copy.deepcopy(self.output_schema)
|
|
113
|
+
return payload
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class CustomToolIssue:
|
|
118
|
+
source_scope: str
|
|
119
|
+
source_path: Path
|
|
120
|
+
relative_tool_path: str
|
|
121
|
+
code: str
|
|
122
|
+
message: str
|
|
123
|
+
tool_name: str = ""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(frozen=True)
|
|
127
|
+
class CustomToolDiscoveryResult:
|
|
128
|
+
global_tools: tuple[CustomToolSpec, ...]
|
|
129
|
+
project_tools: tuple[CustomToolSpec, ...]
|
|
130
|
+
effective_tools: tuple[CustomToolSpec, ...]
|
|
131
|
+
shadowed_tools: tuple[CustomToolSpec, ...]
|
|
132
|
+
issues: tuple[CustomToolIssue, ...]
|
|
133
|
+
|
|
134
|
+
def effective_tools_by_name(self) -> dict[str, CustomToolSpec]:
|
|
135
|
+
return {tool.normalized_name: tool for tool in self.effective_tools}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _config_dir() -> Path:
|
|
139
|
+
override = env_get("ALYSIS_CONFIG_DIR")
|
|
140
|
+
if override:
|
|
141
|
+
return Path(override)
|
|
142
|
+
return canonical_user_config_dir()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def project_custom_tools_root(workspace_root: Path) -> Path:
|
|
146
|
+
return workspace_root.resolve() / ".alysis" / "tools"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def global_custom_tools_root(*, user_config_dir: Path | None = None) -> Path:
|
|
150
|
+
base = _resolved_config_dir(user_config_dir=user_config_dir)
|
|
151
|
+
return base / "tools"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _resolved_config_dir(*, user_config_dir: Path | None = None) -> Path:
|
|
155
|
+
base = user_config_dir.expanduser() if user_config_dir is not None else _config_dir()
|
|
156
|
+
return base.resolve()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def discover_custom_tools(
|
|
160
|
+
*,
|
|
161
|
+
workspace_root: Path,
|
|
162
|
+
built_in_tool_names: set[str] | None = None,
|
|
163
|
+
user_config_dir: Path | None = None,
|
|
164
|
+
env: dict[str, str] | None = None,
|
|
165
|
+
) -> CustomToolDiscoveryResult:
|
|
166
|
+
reserved_names = {
|
|
167
|
+
str(name or "").strip().casefold()
|
|
168
|
+
for name in (built_in_tool_names or set())
|
|
169
|
+
if str(name or "").strip()
|
|
170
|
+
}
|
|
171
|
+
effective_env = dict(os.environ if env is None else env)
|
|
172
|
+
resolved_workspace_root = workspace_root.resolve()
|
|
173
|
+
resolved_config_dir = _resolved_config_dir(user_config_dir=user_config_dir)
|
|
174
|
+
global_root = global_custom_tools_root(user_config_dir=user_config_dir)
|
|
175
|
+
project_root = project_custom_tools_root(workspace_root)
|
|
176
|
+
|
|
177
|
+
global_tools, global_issues = _discover_scope(
|
|
178
|
+
root=global_root,
|
|
179
|
+
source_scope="global",
|
|
180
|
+
owning_base=resolved_config_dir,
|
|
181
|
+
reserved_names=reserved_names,
|
|
182
|
+
env=effective_env,
|
|
183
|
+
)
|
|
184
|
+
project_tools, project_issues = _discover_scope(
|
|
185
|
+
root=project_root,
|
|
186
|
+
source_scope="project",
|
|
187
|
+
owning_base=resolved_workspace_root,
|
|
188
|
+
reserved_names=reserved_names,
|
|
189
|
+
env=effective_env,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
effective: dict[str, CustomToolSpec] = {tool.normalized_name: tool for tool in global_tools}
|
|
193
|
+
shadowed: list[CustomToolSpec] = []
|
|
194
|
+
for tool in project_tools:
|
|
195
|
+
previous = effective.get(tool.normalized_name)
|
|
196
|
+
if previous is not None and previous.source_scope == "global":
|
|
197
|
+
shadowed.append(previous)
|
|
198
|
+
effective[tool.normalized_name] = tool
|
|
199
|
+
|
|
200
|
+
return CustomToolDiscoveryResult(
|
|
201
|
+
global_tools=tuple(global_tools),
|
|
202
|
+
project_tools=tuple(project_tools),
|
|
203
|
+
effective_tools=tuple(
|
|
204
|
+
sorted(effective.values(), key=lambda tool: (tool.name.casefold(), tool.source_scope))
|
|
205
|
+
),
|
|
206
|
+
shadowed_tools=tuple(sorted(shadowed, key=lambda tool: tool.name.casefold())),
|
|
207
|
+
issues=tuple(
|
|
208
|
+
sorted(
|
|
209
|
+
[*global_issues, *project_issues],
|
|
210
|
+
key=lambda issue: (
|
|
211
|
+
issue.tool_name.casefold() if issue.tool_name else "~",
|
|
212
|
+
issue.source_scope,
|
|
213
|
+
issue.relative_tool_path.casefold(),
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _discover_scope(
|
|
221
|
+
*,
|
|
222
|
+
root: Path,
|
|
223
|
+
source_scope: str,
|
|
224
|
+
owning_base: Path,
|
|
225
|
+
reserved_names: set[str],
|
|
226
|
+
env: dict[str, str],
|
|
227
|
+
) -> tuple[list[CustomToolSpec], list[CustomToolIssue]]:
|
|
228
|
+
if not _resolved_path_is_within_scope(root, owning_base):
|
|
229
|
+
return [], [
|
|
230
|
+
_tool_issue(
|
|
231
|
+
source_scope=source_scope,
|
|
232
|
+
source_path=root,
|
|
233
|
+
relative_tool_path=_issue_relative_tool_path(
|
|
234
|
+
root,
|
|
235
|
+
owning_base=owning_base,
|
|
236
|
+
source_scope=source_scope,
|
|
237
|
+
),
|
|
238
|
+
code="path_escape",
|
|
239
|
+
message=_path_escape_message(
|
|
240
|
+
resolved_path=root.resolve(),
|
|
241
|
+
owning_base=owning_base,
|
|
242
|
+
source_scope=source_scope,
|
|
243
|
+
kind="custom tool root",
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
]
|
|
247
|
+
if not root.exists() or not root.is_dir():
|
|
248
|
+
return [], []
|
|
249
|
+
try:
|
|
250
|
+
candidates = sorted(child for child in root.rglob("*.py") if child.is_file())
|
|
251
|
+
except OSError as exc:
|
|
252
|
+
return [], [
|
|
253
|
+
CustomToolIssue(
|
|
254
|
+
source_scope=source_scope,
|
|
255
|
+
source_path=root,
|
|
256
|
+
relative_tool_path=_issue_relative_tool_path(
|
|
257
|
+
root,
|
|
258
|
+
owning_base=owning_base,
|
|
259
|
+
source_scope=source_scope,
|
|
260
|
+
),
|
|
261
|
+
code="scan_failed",
|
|
262
|
+
message=f"failed to scan tool root: {exc}",
|
|
263
|
+
)
|
|
264
|
+
]
|
|
265
|
+
|
|
266
|
+
valid_specs: list[CustomToolSpec] = []
|
|
267
|
+
issues: list[CustomToolIssue] = []
|
|
268
|
+
seen_valid_names: dict[str, CustomToolSpec] = {}
|
|
269
|
+
for path in candidates:
|
|
270
|
+
if path.suffix != ".py":
|
|
271
|
+
continue
|
|
272
|
+
spec, issue = _load_tool_spec(
|
|
273
|
+
path=path,
|
|
274
|
+
source_scope=source_scope,
|
|
275
|
+
root=root,
|
|
276
|
+
owning_base=owning_base,
|
|
277
|
+
reserved_names=reserved_names,
|
|
278
|
+
env=env,
|
|
279
|
+
)
|
|
280
|
+
if issue is not None:
|
|
281
|
+
issues.append(issue)
|
|
282
|
+
continue
|
|
283
|
+
assert spec is not None
|
|
284
|
+
duplicate = seen_valid_names.get(spec.normalized_name)
|
|
285
|
+
if duplicate is not None:
|
|
286
|
+
issues.append(
|
|
287
|
+
CustomToolIssue(
|
|
288
|
+
source_scope=source_scope,
|
|
289
|
+
source_path=spec.source_path,
|
|
290
|
+
relative_tool_path=spec.relative_tool_path,
|
|
291
|
+
code="duplicate_name",
|
|
292
|
+
message=(
|
|
293
|
+
f"duplicate custom tool name '{spec.name}' in {source_scope} scope; "
|
|
294
|
+
f"already provided by {duplicate.relative_tool_path}"
|
|
295
|
+
),
|
|
296
|
+
tool_name=spec.name,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
continue
|
|
300
|
+
seen_valid_names[spec.normalized_name] = spec
|
|
301
|
+
valid_specs.append(spec)
|
|
302
|
+
return valid_specs, issues
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _load_tool_spec(
|
|
306
|
+
*,
|
|
307
|
+
path: Path,
|
|
308
|
+
source_scope: str,
|
|
309
|
+
root: Path,
|
|
310
|
+
owning_base: Path,
|
|
311
|
+
reserved_names: set[str],
|
|
312
|
+
env: dict[str, str],
|
|
313
|
+
) -> tuple[CustomToolSpec | None, CustomToolIssue | None]:
|
|
314
|
+
relative_tool_path = _issue_relative_tool_path(
|
|
315
|
+
path,
|
|
316
|
+
owning_base=owning_base,
|
|
317
|
+
source_scope=source_scope,
|
|
318
|
+
)
|
|
319
|
+
if not _resolved_path_is_within_scope(path, owning_base):
|
|
320
|
+
return None, _tool_issue(
|
|
321
|
+
source_scope=source_scope,
|
|
322
|
+
source_path=path,
|
|
323
|
+
relative_tool_path=relative_tool_path,
|
|
324
|
+
code="path_escape",
|
|
325
|
+
message=_path_escape_message(
|
|
326
|
+
resolved_path=path.resolve(),
|
|
327
|
+
owning_base=owning_base,
|
|
328
|
+
source_scope=source_scope,
|
|
329
|
+
kind="custom tool source path",
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
if path.is_symlink():
|
|
333
|
+
return None, _tool_issue(
|
|
334
|
+
source_scope=source_scope,
|
|
335
|
+
source_path=path,
|
|
336
|
+
relative_tool_path=relative_tool_path,
|
|
337
|
+
code="symlink_rejected",
|
|
338
|
+
message="symlinked tool files are not supported",
|
|
339
|
+
)
|
|
340
|
+
try:
|
|
341
|
+
raw_bytes = path.read_bytes()
|
|
342
|
+
except OSError as exc:
|
|
343
|
+
return None, _tool_issue(
|
|
344
|
+
source_scope=source_scope,
|
|
345
|
+
source_path=path,
|
|
346
|
+
relative_tool_path=relative_tool_path,
|
|
347
|
+
code="read_failed",
|
|
348
|
+
message=f"failed to read tool file: {exc}",
|
|
349
|
+
)
|
|
350
|
+
try:
|
|
351
|
+
source_text = raw_bytes.decode("utf-8")
|
|
352
|
+
except UnicodeDecodeError:
|
|
353
|
+
return None, _tool_issue(
|
|
354
|
+
source_scope=source_scope,
|
|
355
|
+
source_path=path,
|
|
356
|
+
relative_tool_path=relative_tool_path,
|
|
357
|
+
code="invalid_utf8",
|
|
358
|
+
message="tool file must be valid UTF-8",
|
|
359
|
+
)
|
|
360
|
+
if len(raw_bytes) > _MAX_MANIFEST_BYTES * 4:
|
|
361
|
+
return None, _tool_issue(
|
|
362
|
+
source_scope=source_scope,
|
|
363
|
+
source_path=path,
|
|
364
|
+
relative_tool_path=relative_tool_path,
|
|
365
|
+
code="file_too_large",
|
|
366
|
+
message="tool file is too large",
|
|
367
|
+
)
|
|
368
|
+
try:
|
|
369
|
+
tree = ast.parse(source_text, filename=os.fspath(path))
|
|
370
|
+
except SyntaxError as exc:
|
|
371
|
+
return None, _tool_issue(
|
|
372
|
+
source_scope=source_scope,
|
|
373
|
+
source_path=path,
|
|
374
|
+
relative_tool_path=relative_tool_path,
|
|
375
|
+
code="syntax_error",
|
|
376
|
+
message=f"syntax error: {exc.msg}",
|
|
377
|
+
)
|
|
378
|
+
manifest_value, manifest_error = _extract_tool_manifest(tree)
|
|
379
|
+
if manifest_error is not None:
|
|
380
|
+
return None, _tool_issue(
|
|
381
|
+
source_scope=source_scope,
|
|
382
|
+
source_path=path,
|
|
383
|
+
relative_tool_path=relative_tool_path,
|
|
384
|
+
code=manifest_error[0],
|
|
385
|
+
message=manifest_error[1],
|
|
386
|
+
)
|
|
387
|
+
assert manifest_value is not None
|
|
388
|
+
if not _has_valid_run_function(tree):
|
|
389
|
+
tool_name = str(manifest_value.get("name") or "")
|
|
390
|
+
return None, _tool_issue(
|
|
391
|
+
source_scope=source_scope,
|
|
392
|
+
source_path=path,
|
|
393
|
+
relative_tool_path=relative_tool_path,
|
|
394
|
+
code="missing_run",
|
|
395
|
+
message="tool file must define top-level def run(args)",
|
|
396
|
+
tool_name=tool_name,
|
|
397
|
+
)
|
|
398
|
+
try:
|
|
399
|
+
spec = _build_tool_spec(
|
|
400
|
+
manifest=manifest_value,
|
|
401
|
+
raw_bytes=raw_bytes,
|
|
402
|
+
source_scope=source_scope,
|
|
403
|
+
source_path=path,
|
|
404
|
+
relative_tool_path=_spec_relative_tool_path(
|
|
405
|
+
path,
|
|
406
|
+
root=root,
|
|
407
|
+
owning_base=owning_base,
|
|
408
|
+
source_scope=source_scope,
|
|
409
|
+
),
|
|
410
|
+
reserved_names=reserved_names,
|
|
411
|
+
env=env,
|
|
412
|
+
)
|
|
413
|
+
except ValueError as exc:
|
|
414
|
+
tool_name = ""
|
|
415
|
+
raw_name = manifest_value.get("name")
|
|
416
|
+
if isinstance(raw_name, str):
|
|
417
|
+
tool_name = raw_name
|
|
418
|
+
return None, _tool_issue(
|
|
419
|
+
source_scope=source_scope,
|
|
420
|
+
source_path=path,
|
|
421
|
+
relative_tool_path=relative_tool_path,
|
|
422
|
+
code="invalid_manifest",
|
|
423
|
+
message=str(exc),
|
|
424
|
+
tool_name=tool_name,
|
|
425
|
+
)
|
|
426
|
+
return spec, None
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _extract_tool_manifest(
|
|
430
|
+
tree: ast.Module,
|
|
431
|
+
) -> tuple[dict[str, Any] | None, tuple[str, str] | None]:
|
|
432
|
+
manifest_nodes: list[ast.AST] = []
|
|
433
|
+
for statement in tree.body:
|
|
434
|
+
if isinstance(statement, ast.Assign):
|
|
435
|
+
for target in statement.targets:
|
|
436
|
+
if isinstance(target, ast.Name) and target.id == "TOOL":
|
|
437
|
+
manifest_nodes.append(statement.value)
|
|
438
|
+
elif isinstance(statement, ast.AnnAssign):
|
|
439
|
+
if isinstance(statement.target, ast.Name) and statement.target.id == "TOOL":
|
|
440
|
+
manifest_nodes.append(statement.value)
|
|
441
|
+
if not manifest_nodes:
|
|
442
|
+
return None, ("missing_manifest", "tool file must define top-level TOOL = {...}")
|
|
443
|
+
if len(manifest_nodes) > 1:
|
|
444
|
+
return None, ("duplicate_manifest", "tool file defines TOOL more than once")
|
|
445
|
+
try:
|
|
446
|
+
manifest_value = ast.literal_eval(manifest_nodes[0])
|
|
447
|
+
except (SyntaxError, ValueError) as exc:
|
|
448
|
+
return None, ("invalid_manifest", f"TOOL must be a top-level literal dictionary: {exc}")
|
|
449
|
+
if not isinstance(manifest_value, dict):
|
|
450
|
+
return None, ("invalid_manifest", "TOOL must be a top-level literal dictionary")
|
|
451
|
+
try:
|
|
452
|
+
manifest_bytes = _stable_json_size(manifest_value)
|
|
453
|
+
except TypeError as exc:
|
|
454
|
+
return None, ("invalid_manifest", f"TOOL must be JSON-serializable: {exc}")
|
|
455
|
+
if manifest_bytes > _MAX_MANIFEST_BYTES:
|
|
456
|
+
return None, ("invalid_manifest", "TOOL manifest is too large")
|
|
457
|
+
return copy.deepcopy(manifest_value), None
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _has_valid_run_function(tree: ast.Module) -> bool:
|
|
461
|
+
for statement in tree.body:
|
|
462
|
+
if not isinstance(statement, ast.FunctionDef):
|
|
463
|
+
continue
|
|
464
|
+
if statement.name != "run":
|
|
465
|
+
continue
|
|
466
|
+
args = statement.args
|
|
467
|
+
positional = [*args.posonlyargs, *args.args]
|
|
468
|
+
if len(positional) != 1:
|
|
469
|
+
return False
|
|
470
|
+
if args.vararg is not None or args.kwarg is not None or args.kwonlyargs:
|
|
471
|
+
return False
|
|
472
|
+
return True
|
|
473
|
+
return False
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _build_tool_spec(
|
|
477
|
+
*,
|
|
478
|
+
manifest: dict[str, Any],
|
|
479
|
+
raw_bytes: bytes,
|
|
480
|
+
source_scope: str,
|
|
481
|
+
source_path: Path,
|
|
482
|
+
relative_tool_path: str,
|
|
483
|
+
reserved_names: set[str],
|
|
484
|
+
env: dict[str, str],
|
|
485
|
+
) -> CustomToolSpec:
|
|
486
|
+
required_keys = {"name", "description", "input_schema"}
|
|
487
|
+
allowed_keys = {
|
|
488
|
+
"manifest_version",
|
|
489
|
+
"name",
|
|
490
|
+
"description",
|
|
491
|
+
"input_schema",
|
|
492
|
+
"output_schema",
|
|
493
|
+
"capabilities",
|
|
494
|
+
"timeout_s",
|
|
495
|
+
"required_env",
|
|
496
|
+
"enabled_in",
|
|
497
|
+
"isolation",
|
|
498
|
+
}
|
|
499
|
+
missing_keys = sorted(required_keys - set(manifest))
|
|
500
|
+
if missing_keys:
|
|
501
|
+
raise ValueError(f"missing required TOOL keys: {', '.join(missing_keys)}")
|
|
502
|
+
unknown_keys = sorted(set(manifest) - allowed_keys)
|
|
503
|
+
if unknown_keys:
|
|
504
|
+
raise ValueError(f"unsupported TOOL keys: {', '.join(unknown_keys)}")
|
|
505
|
+
|
|
506
|
+
name = str(manifest.get("name") or "").strip()
|
|
507
|
+
if not name:
|
|
508
|
+
raise ValueError("TOOL.name must be a non-empty string")
|
|
509
|
+
normalized_name = name.casefold()
|
|
510
|
+
if normalized_name in _RESERVED_EXACT_NAMES:
|
|
511
|
+
raise ValueError(f"TOOL.name collides with reserved host tool name: {name}")
|
|
512
|
+
if normalized_name in reserved_names:
|
|
513
|
+
raise ValueError(f"TOOL.name collides with built-in tool name: {name}")
|
|
514
|
+
for prefix in _RESERVED_NAME_PREFIXES:
|
|
515
|
+
if normalized_name.startswith(prefix):
|
|
516
|
+
raise ValueError(f"TOOL.name uses reserved prefix '{prefix}': {name}")
|
|
517
|
+
|
|
518
|
+
description = str(manifest.get("description") or "").strip()
|
|
519
|
+
if not description:
|
|
520
|
+
raise ValueError("TOOL.description must be a non-empty string")
|
|
521
|
+
if len(description) > _MAX_DESCRIPTION_CHARS:
|
|
522
|
+
raise ValueError("TOOL.description is too long")
|
|
523
|
+
|
|
524
|
+
manifest_version = _validate_manifest_version(manifest.get("manifest_version", 1))
|
|
525
|
+
input_schema = _validate_input_schema(manifest.get("input_schema"))
|
|
526
|
+
output_schema = _validate_output_schema(manifest.get("output_schema"))
|
|
527
|
+
timeout_s = _coerce_timeout(manifest.get("timeout_s", _DEFAULT_TIMEOUT_S))
|
|
528
|
+
required_env = _validate_required_env(manifest.get("required_env", ()))
|
|
529
|
+
capabilities = _validate_capabilities(
|
|
530
|
+
manifest.get("capabilities", {}),
|
|
531
|
+
required_env=required_env,
|
|
532
|
+
)
|
|
533
|
+
enabled_in = _validate_enabled_in(manifest.get("enabled_in", _DEFAULT_ENABLED_IN))
|
|
534
|
+
isolation = _validate_isolation(manifest.get("isolation", "subprocess"))
|
|
535
|
+
file_hash = hashlib.sha256(raw_bytes).hexdigest()
|
|
536
|
+
missing_env = tuple(
|
|
537
|
+
name for name in required_env if not str(env_get_from(env, name) or "").strip()
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
return CustomToolSpec(
|
|
541
|
+
name=name,
|
|
542
|
+
description=description,
|
|
543
|
+
input_schema=input_schema,
|
|
544
|
+
manifest_version=manifest_version,
|
|
545
|
+
capabilities=capabilities,
|
|
546
|
+
output_schema=output_schema,
|
|
547
|
+
timeout_s=timeout_s,
|
|
548
|
+
required_env=required_env,
|
|
549
|
+
enabled_in=enabled_in,
|
|
550
|
+
isolation=isolation,
|
|
551
|
+
source_scope=source_scope,
|
|
552
|
+
source_path=source_path.resolve(),
|
|
553
|
+
relative_tool_path=relative_tool_path,
|
|
554
|
+
file_hash=file_hash,
|
|
555
|
+
missing_env=missing_env,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _validate_manifest_version(value: Any) -> int:
|
|
560
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
561
|
+
raise ValueError("TOOL.manifest_version must be an integer")
|
|
562
|
+
if value != _SUPPORTED_MANIFEST_VERSION:
|
|
563
|
+
raise ValueError(f"unsupported TOOL.manifest_version: {value}")
|
|
564
|
+
return value
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _validate_input_schema(value: Any) -> dict[str, Any]:
|
|
568
|
+
if not isinstance(value, dict):
|
|
569
|
+
raise ValueError("TOOL.input_schema must be a JSON object")
|
|
570
|
+
if value.get("type") != "object":
|
|
571
|
+
raise ValueError("TOOL.input_schema root type must be 'object'")
|
|
572
|
+
properties = value.get("properties", {})
|
|
573
|
+
if not isinstance(properties, dict):
|
|
574
|
+
raise ValueError("TOOL.input_schema.properties must be an object")
|
|
575
|
+
required = value.get("required", [])
|
|
576
|
+
if not isinstance(required, list) or any(not isinstance(item, str) for item in required):
|
|
577
|
+
raise ValueError("TOOL.input_schema.required must be an array of strings")
|
|
578
|
+
try:
|
|
579
|
+
schema_bytes = _stable_json_size(value)
|
|
580
|
+
except TypeError as exc:
|
|
581
|
+
raise ValueError(f"TOOL.input_schema must be JSON-serializable: {exc}") from exc
|
|
582
|
+
if schema_bytes > _MAX_SCHEMA_BYTES:
|
|
583
|
+
raise ValueError("TOOL.input_schema is too large")
|
|
584
|
+
return copy.deepcopy(value)
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _validate_output_schema(value: Any) -> dict[str, Any] | None:
|
|
588
|
+
if value is None:
|
|
589
|
+
return None
|
|
590
|
+
if not isinstance(value, dict):
|
|
591
|
+
raise ValueError("TOOL.output_schema must be a JSON object")
|
|
592
|
+
if value.get("type") != "object":
|
|
593
|
+
raise ValueError("TOOL.output_schema root type must be 'object'")
|
|
594
|
+
try:
|
|
595
|
+
schema_bytes = _stable_json_size(value)
|
|
596
|
+
except TypeError as exc:
|
|
597
|
+
raise ValueError(f"TOOL.output_schema must be JSON-serializable: {exc}") from exc
|
|
598
|
+
if schema_bytes > _MAX_SCHEMA_BYTES:
|
|
599
|
+
raise ValueError("TOOL.output_schema is too large")
|
|
600
|
+
return copy.deepcopy(value)
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _validate_capabilities(
|
|
604
|
+
value: Any,
|
|
605
|
+
*,
|
|
606
|
+
required_env: tuple[str, ...],
|
|
607
|
+
) -> CustomToolCapabilities:
|
|
608
|
+
if value in (None, ()):
|
|
609
|
+
value = {}
|
|
610
|
+
if not isinstance(value, dict):
|
|
611
|
+
raise ValueError("TOOL.capabilities must be a JSON object")
|
|
612
|
+
allowed_keys = {
|
|
613
|
+
"read_only",
|
|
614
|
+
"destructive",
|
|
615
|
+
"network_access",
|
|
616
|
+
"network_hosts",
|
|
617
|
+
"network",
|
|
618
|
+
"filesystem",
|
|
619
|
+
"filesystem_read_scope",
|
|
620
|
+
"filesystem_write_scope",
|
|
621
|
+
"process_spawn",
|
|
622
|
+
"secret_refs",
|
|
623
|
+
}
|
|
624
|
+
unknown_keys = sorted(set(value) - allowed_keys)
|
|
625
|
+
if unknown_keys:
|
|
626
|
+
raise ValueError(f"unsupported TOOL.capabilities keys: {', '.join(unknown_keys)}")
|
|
627
|
+
|
|
628
|
+
filesystem = value.get("filesystem", {})
|
|
629
|
+
if filesystem in (None, ()):
|
|
630
|
+
filesystem = {}
|
|
631
|
+
if not isinstance(filesystem, dict):
|
|
632
|
+
raise ValueError("TOOL.capabilities.filesystem must be a JSON object")
|
|
633
|
+
filesystem_allowed = {"read", "write", "read_scope", "write_scope"}
|
|
634
|
+
filesystem_unknown = sorted(set(filesystem) - filesystem_allowed)
|
|
635
|
+
if filesystem_unknown:
|
|
636
|
+
raise ValueError(
|
|
637
|
+
f"unsupported TOOL.capabilities.filesystem keys: {', '.join(filesystem_unknown)}"
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
read_only = _coerce_bool(value.get("read_only", False), "TOOL.capabilities.read_only")
|
|
641
|
+
destructive = _coerce_bool(value.get("destructive", False), "TOOL.capabilities.destructive")
|
|
642
|
+
if read_only and destructive:
|
|
643
|
+
raise ValueError("TOOL.capabilities cannot be both read_only and destructive")
|
|
644
|
+
|
|
645
|
+
network_access = _validate_enum_value(
|
|
646
|
+
value.get("network_access", value.get("network", "unspecified")),
|
|
647
|
+
allowed=_VALID_NETWORK_ACCESS_VALUES,
|
|
648
|
+
label="TOOL.capabilities.network_access",
|
|
649
|
+
)
|
|
650
|
+
network_hosts = _validate_network_hosts(value.get("network_hosts", ()))
|
|
651
|
+
if network_access == "restricted" and not network_hosts:
|
|
652
|
+
raise ValueError(
|
|
653
|
+
"TOOL.capabilities.network_hosts is required when network_access is restricted"
|
|
654
|
+
)
|
|
655
|
+
if network_access != "restricted" and network_hosts:
|
|
656
|
+
raise ValueError(
|
|
657
|
+
"TOOL.capabilities.network_hosts is only valid with restricted network access"
|
|
658
|
+
)
|
|
659
|
+
filesystem_read_scope = _validate_enum_value(
|
|
660
|
+
value.get(
|
|
661
|
+
"filesystem_read_scope",
|
|
662
|
+
filesystem.get("read", filesystem.get("read_scope", "unspecified")),
|
|
663
|
+
),
|
|
664
|
+
allowed=_VALID_FILESYSTEM_SCOPE_VALUES,
|
|
665
|
+
label="TOOL.capabilities.filesystem_read_scope",
|
|
666
|
+
)
|
|
667
|
+
filesystem_write_scope = _validate_enum_value(
|
|
668
|
+
value.get(
|
|
669
|
+
"filesystem_write_scope",
|
|
670
|
+
filesystem.get("write", filesystem.get("write_scope", "unspecified")),
|
|
671
|
+
),
|
|
672
|
+
allowed=_VALID_FILESYSTEM_SCOPE_VALUES,
|
|
673
|
+
label="TOOL.capabilities.filesystem_write_scope",
|
|
674
|
+
)
|
|
675
|
+
if read_only and filesystem_write_scope not in {"none", "unspecified"}:
|
|
676
|
+
raise ValueError(
|
|
677
|
+
"TOOL.capabilities.read_only requires filesystem_write_scope to be none or unspecified"
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
process_spawn = _validate_enum_value(
|
|
681
|
+
value.get("process_spawn", "unspecified"),
|
|
682
|
+
allowed=_VALID_PROCESS_SPAWN_VALUES,
|
|
683
|
+
label="TOOL.capabilities.process_spawn",
|
|
684
|
+
)
|
|
685
|
+
secret_refs = _validate_secret_refs(value.get("secret_refs", ()))
|
|
686
|
+
if required_env:
|
|
687
|
+
secret_refs = _dedupe_strings([*secret_refs, *required_env])
|
|
688
|
+
|
|
689
|
+
return CustomToolCapabilities(
|
|
690
|
+
read_only=read_only,
|
|
691
|
+
destructive=destructive,
|
|
692
|
+
network_access=network_access,
|
|
693
|
+
network_hosts=network_hosts,
|
|
694
|
+
filesystem_read_scope=filesystem_read_scope,
|
|
695
|
+
filesystem_write_scope=filesystem_write_scope,
|
|
696
|
+
process_spawn=process_spawn,
|
|
697
|
+
secret_refs=secret_refs,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _coerce_bool(value: Any, label: str) -> bool:
|
|
702
|
+
if not isinstance(value, bool):
|
|
703
|
+
raise ValueError(f"{label} must be true or false")
|
|
704
|
+
return value
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _validate_enum_value(value: Any, *, allowed: set[str], label: str) -> str:
|
|
708
|
+
normalized = str(value or "").strip().lower()
|
|
709
|
+
if normalized not in allowed:
|
|
710
|
+
raise ValueError(f"{label} must be one of: {', '.join(sorted(allowed))}")
|
|
711
|
+
return normalized
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _validate_secret_refs(value: Any) -> tuple[str, ...]:
|
|
715
|
+
if value in (None, ()):
|
|
716
|
+
return ()
|
|
717
|
+
if not isinstance(value, list):
|
|
718
|
+
raise ValueError("TOOL.capabilities.secret_refs must be an array of env var names")
|
|
719
|
+
names: list[str] = []
|
|
720
|
+
for item in value:
|
|
721
|
+
name = str(item or "").strip()
|
|
722
|
+
if not name:
|
|
723
|
+
raise ValueError("TOOL.capabilities.secret_refs cannot contain empty values")
|
|
724
|
+
if not _ENV_NAME_RE.fullmatch(name):
|
|
725
|
+
raise ValueError(f"TOOL.capabilities.secret_refs contains invalid env var name: {name}")
|
|
726
|
+
names.append(name)
|
|
727
|
+
if len(names) > _MAX_SECRET_REFS:
|
|
728
|
+
raise ValueError("TOOL.capabilities.secret_refs contains too many entries")
|
|
729
|
+
return _dedupe_strings(names)
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _validate_network_hosts(value: Any) -> tuple[str, ...]:
|
|
733
|
+
if value in (None, ()):
|
|
734
|
+
return ()
|
|
735
|
+
if not isinstance(value, list):
|
|
736
|
+
raise ValueError("TOOL.capabilities.network_hosts must be an array of host strings")
|
|
737
|
+
hosts: list[str] = []
|
|
738
|
+
for item in value:
|
|
739
|
+
if not isinstance(item, str):
|
|
740
|
+
raise ValueError("TOOL.capabilities.network_hosts must contain only strings")
|
|
741
|
+
host = item.strip()
|
|
742
|
+
if not host:
|
|
743
|
+
raise ValueError("TOOL.capabilities.network_hosts cannot contain empty values")
|
|
744
|
+
if "*" in host:
|
|
745
|
+
raise ValueError("TOOL.capabilities.network_hosts does not support wildcards")
|
|
746
|
+
if any(char.isspace() for char in host) or "/" in host:
|
|
747
|
+
raise ValueError(
|
|
748
|
+
"TOOL.capabilities.network_hosts entries must be exact host or IP strings"
|
|
749
|
+
)
|
|
750
|
+
hosts.append(host)
|
|
751
|
+
if len(hosts) > _MAX_NETWORK_HOSTS:
|
|
752
|
+
raise ValueError("TOOL.capabilities.network_hosts contains too many entries")
|
|
753
|
+
return _dedupe_strings(hosts)
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _dedupe_strings(values: list[str]) -> tuple[str, ...]:
|
|
757
|
+
result: list[str] = []
|
|
758
|
+
seen: set[str] = set()
|
|
759
|
+
for value in values:
|
|
760
|
+
if value in seen:
|
|
761
|
+
continue
|
|
762
|
+
seen.add(value)
|
|
763
|
+
result.append(value)
|
|
764
|
+
return tuple(result)
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _validate_required_env(value: Any) -> tuple[str, ...]:
|
|
768
|
+
if value in (None, ()):
|
|
769
|
+
return ()
|
|
770
|
+
if not isinstance(value, list):
|
|
771
|
+
raise ValueError("TOOL.required_env must be an array of env var names")
|
|
772
|
+
names: list[str] = []
|
|
773
|
+
seen: set[str] = set()
|
|
774
|
+
for item in value:
|
|
775
|
+
name = str(item or "").strip()
|
|
776
|
+
if not name:
|
|
777
|
+
raise ValueError("TOOL.required_env cannot contain empty values")
|
|
778
|
+
if not _ENV_NAME_RE.fullmatch(name):
|
|
779
|
+
raise ValueError(f"TOOL.required_env contains invalid env var name: {name}")
|
|
780
|
+
if name in seen:
|
|
781
|
+
continue
|
|
782
|
+
seen.add(name)
|
|
783
|
+
names.append(name)
|
|
784
|
+
if len(names) > _MAX_REQUIRED_ENV_VARS:
|
|
785
|
+
raise ValueError("TOOL.required_env contains too many entries")
|
|
786
|
+
return tuple(names)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _validate_enabled_in(value: Any) -> tuple[str, ...]:
|
|
790
|
+
if not isinstance(value, list) and not isinstance(value, tuple):
|
|
791
|
+
raise ValueError("TOOL.enabled_in must be an array of runtime kinds")
|
|
792
|
+
allowed = set(runtime_kind_values())
|
|
793
|
+
items: list[str] = []
|
|
794
|
+
seen: set[str] = set()
|
|
795
|
+
for item in value:
|
|
796
|
+
runtime = str(item or "").strip().lower()
|
|
797
|
+
if runtime not in allowed:
|
|
798
|
+
raise ValueError(
|
|
799
|
+
"TOOL.enabled_in contains invalid runtime kind: "
|
|
800
|
+
f"{item!r}. Expected one of: {', '.join(runtime_kind_values())}"
|
|
801
|
+
)
|
|
802
|
+
if runtime in seen:
|
|
803
|
+
continue
|
|
804
|
+
seen.add(runtime)
|
|
805
|
+
items.append(runtime)
|
|
806
|
+
if not items:
|
|
807
|
+
raise ValueError("TOOL.enabled_in cannot be empty")
|
|
808
|
+
return tuple(items)
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
def _validate_isolation(value: Any) -> str:
|
|
812
|
+
isolation = str(value or "").strip().lower()
|
|
813
|
+
if isolation not in _VALID_ISOLATION_VALUES:
|
|
814
|
+
raise ValueError("TOOL.isolation must be subprocess; inprocess is not supported")
|
|
815
|
+
return isolation
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
def _coerce_timeout(value: Any) -> float:
|
|
819
|
+
try:
|
|
820
|
+
timeout = float(value)
|
|
821
|
+
except (TypeError, ValueError) as exc:
|
|
822
|
+
raise ValueError("TOOL.timeout_s must be a positive number") from exc
|
|
823
|
+
if timeout <= 0:
|
|
824
|
+
raise ValueError("TOOL.timeout_s must be positive")
|
|
825
|
+
if timeout > _MAX_TIMEOUT_S:
|
|
826
|
+
raise ValueError(f"TOOL.timeout_s must be <= {_MAX_TIMEOUT_S}")
|
|
827
|
+
return timeout
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def _spec_relative_tool_path(
|
|
831
|
+
path: Path,
|
|
832
|
+
root: Path,
|
|
833
|
+
owning_base: Path,
|
|
834
|
+
source_scope: str,
|
|
835
|
+
) -> str:
|
|
836
|
+
if source_scope == "project":
|
|
837
|
+
return _issue_relative_tool_path(path, owning_base=owning_base, source_scope=source_scope)
|
|
838
|
+
return path.resolve().relative_to(root.resolve()).as_posix()
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _issue_relative_tool_path(
|
|
842
|
+
path: Path,
|
|
843
|
+
*,
|
|
844
|
+
owning_base: Path,
|
|
845
|
+
source_scope: str,
|
|
846
|
+
) -> str:
|
|
847
|
+
try:
|
|
848
|
+
return path.relative_to(owning_base).as_posix()
|
|
849
|
+
except ValueError:
|
|
850
|
+
scope_root = _scope_root_label(source_scope)
|
|
851
|
+
name = path.name.strip()
|
|
852
|
+
if not name or name == Path(scope_root).name:
|
|
853
|
+
return scope_root
|
|
854
|
+
return f"{scope_root}/{name}"
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def _scope_root_label(source_scope: str) -> str:
|
|
858
|
+
return ".alysis/tools" if source_scope == "project" else "tools"
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _resolved_path_is_within_scope(path: Path, owning_base: Path) -> bool:
|
|
862
|
+
try:
|
|
863
|
+
path.resolve().relative_to(owning_base)
|
|
864
|
+
except ValueError:
|
|
865
|
+
return False
|
|
866
|
+
return True
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def _path_escape_message(
|
|
870
|
+
*,
|
|
871
|
+
resolved_path: Path,
|
|
872
|
+
owning_base: Path,
|
|
873
|
+
source_scope: str,
|
|
874
|
+
kind: str,
|
|
875
|
+
) -> str:
|
|
876
|
+
scope_label = "workspace-owned" if source_scope == "project" else "config-owned"
|
|
877
|
+
return (
|
|
878
|
+
f"resolved {kind} is outside the {scope_label} custom tools scope: "
|
|
879
|
+
f"{resolved_path} (expected under {owning_base})"
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _tool_issue(
|
|
884
|
+
*,
|
|
885
|
+
source_scope: str,
|
|
886
|
+
source_path: Path,
|
|
887
|
+
relative_tool_path: str,
|
|
888
|
+
code: str,
|
|
889
|
+
message: str,
|
|
890
|
+
tool_name: str = "",
|
|
891
|
+
) -> CustomToolIssue:
|
|
892
|
+
return CustomToolIssue(
|
|
893
|
+
source_scope=source_scope,
|
|
894
|
+
source_path=source_path.resolve(),
|
|
895
|
+
relative_tool_path=relative_tool_path,
|
|
896
|
+
code=code,
|
|
897
|
+
message=message,
|
|
898
|
+
tool_name=tool_name,
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def _stable_json_size(value: object) -> int:
|
|
903
|
+
return len(json.dumps(value, ensure_ascii=True, sort_keys=True, separators=(",", ":")))
|