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,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .mcp_sanitize import MAX_MCP_TOOL_DESCRIPTION_CHARS, sanitize_mcp_tool_description
|
|
4
|
+
from .safe_http import SafeHttpError, safe_http_request
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"MAX_MCP_TOOL_DESCRIPTION_CHARS",
|
|
8
|
+
"SafeHttpError",
|
|
9
|
+
"safe_http_request",
|
|
10
|
+
"sanitize_mcp_tool_description",
|
|
11
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
MAX_MCP_TOOL_DESCRIPTION_CHARS = 2000
|
|
4
|
+
|
|
5
|
+
_STRIPPED_MCP_TOOL_DESCRIPTION_PREFIXES = (
|
|
6
|
+
"ignore previous",
|
|
7
|
+
"system:",
|
|
8
|
+
"user:",
|
|
9
|
+
"assistant:",
|
|
10
|
+
"you are now",
|
|
11
|
+
"new instructions:",
|
|
12
|
+
"<system>",
|
|
13
|
+
"</system>",
|
|
14
|
+
"[inst]",
|
|
15
|
+
"[/inst]",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def sanitize_mcp_tool_description(description: str) -> str:
|
|
20
|
+
kept_lines: list[str] = []
|
|
21
|
+
for line in str(description or "").splitlines():
|
|
22
|
+
normalized = line.lstrip().casefold()
|
|
23
|
+
if any(normalized.startswith(prefix) for prefix in _STRIPPED_MCP_TOOL_DESCRIPTION_PREFIXES):
|
|
24
|
+
continue
|
|
25
|
+
kept_lines.append(line)
|
|
26
|
+
body = "\n".join(kept_lines).strip()
|
|
27
|
+
if len(body) > MAX_MCP_TOOL_DESCRIPTION_CHARS:
|
|
28
|
+
body = body[:MAX_MCP_TOOL_DESCRIPTION_CHARS].rstrip()
|
|
29
|
+
return f"[mcp-tool-description begin]\n{body}\n[mcp-tool-description end]"
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ipaddress
|
|
4
|
+
import socket
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import urljoin, urlsplit, urlunsplit
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SafeHttpError(Exception):
|
|
14
|
+
"""``recoverable=True`` marks failures specific to the requested URL or host
|
|
15
|
+
(a different target can succeed: oversized body, redirect problems, blocked
|
|
16
|
+
or invalid host). The default ``False`` marks environment-level failures
|
|
17
|
+
(network/DNS outage, invalid caller arguments)."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str, *, recoverable: bool = False) -> None:
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.recoverable = recoverable
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_ALLOWED_SCHEMES = {"http", "https"}
|
|
25
|
+
_REDIRECT_STATUS_CODES = {301, 302, 303, 307, 308}
|
|
26
|
+
_DEFAULT_ACCEPT_ENCODING = "identity"
|
|
27
|
+
Resolver = Callable[[str, int], list[str]]
|
|
28
|
+
_DENIED_NETWORKS = tuple(
|
|
29
|
+
ipaddress.ip_network(network)
|
|
30
|
+
for network in (
|
|
31
|
+
"127.0.0.0/8",
|
|
32
|
+
"::1/128",
|
|
33
|
+
"169.254.0.0/16",
|
|
34
|
+
"fe80::/10",
|
|
35
|
+
"10.0.0.0/8",
|
|
36
|
+
"172.16.0.0/12",
|
|
37
|
+
"192.168.0.0/16",
|
|
38
|
+
"fc00::/7",
|
|
39
|
+
"0.0.0.0/32",
|
|
40
|
+
"::/128",
|
|
41
|
+
"224.0.0.0/4",
|
|
42
|
+
"ff00::/8",
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class _SafeTarget:
|
|
49
|
+
request_url: str
|
|
50
|
+
connect_url: str
|
|
51
|
+
host_header: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def safe_http_request(
|
|
55
|
+
method: str,
|
|
56
|
+
url: str,
|
|
57
|
+
*,
|
|
58
|
+
timeout: float = 30.0,
|
|
59
|
+
max_bytes: int = 10 * 1024 * 1024,
|
|
60
|
+
allow_redirects: bool = True,
|
|
61
|
+
max_redirects: int = 5,
|
|
62
|
+
headers: dict[str, str] | None = None,
|
|
63
|
+
json: Any = None,
|
|
64
|
+
content: bytes | None = None,
|
|
65
|
+
_transport: httpx.AsyncBaseTransport | None = None,
|
|
66
|
+
_resolver: Resolver | None = None,
|
|
67
|
+
) -> httpx.Response:
|
|
68
|
+
if max_bytes < 0:
|
|
69
|
+
raise SafeHttpError("max_bytes must be non-negative.")
|
|
70
|
+
if max_redirects < 0:
|
|
71
|
+
raise SafeHttpError("max_redirects must be non-negative.")
|
|
72
|
+
|
|
73
|
+
current_url = str(url or "").strip()
|
|
74
|
+
if not current_url:
|
|
75
|
+
raise SafeHttpError("url must be a non-empty string.")
|
|
76
|
+
current_method = str(method or "").strip().upper() or "GET"
|
|
77
|
+
request_headers = _headers_with_default_accept_encoding(headers)
|
|
78
|
+
redirects_seen = 0
|
|
79
|
+
request_json = json
|
|
80
|
+
request_content = content
|
|
81
|
+
|
|
82
|
+
async with httpx.AsyncClient(
|
|
83
|
+
follow_redirects=False,
|
|
84
|
+
timeout=httpx.Timeout(timeout),
|
|
85
|
+
transport=_transport,
|
|
86
|
+
) as client:
|
|
87
|
+
while True:
|
|
88
|
+
target = _validate_and_resolve_target(
|
|
89
|
+
current_url,
|
|
90
|
+
resolver=_resolver or _resolve_host_addresses,
|
|
91
|
+
use_resolved_connect_url=_transport is None,
|
|
92
|
+
)
|
|
93
|
+
try:
|
|
94
|
+
response = await _single_request(
|
|
95
|
+
client=client,
|
|
96
|
+
method=current_method,
|
|
97
|
+
target=target,
|
|
98
|
+
headers=request_headers,
|
|
99
|
+
json=request_json,
|
|
100
|
+
content=request_content,
|
|
101
|
+
max_bytes=max_bytes,
|
|
102
|
+
)
|
|
103
|
+
except httpx.DecodingError as exc:
|
|
104
|
+
raise SafeHttpError(
|
|
105
|
+
"Response decompression failed: server or proxy returned invalid "
|
|
106
|
+
"Content-Encoding bytes. safe_http_request sends "
|
|
107
|
+
"Accept-Encoding: identity by default; check upstream compression handling.",
|
|
108
|
+
recoverable=True,
|
|
109
|
+
) from exc
|
|
110
|
+
if not allow_redirects or response.status_code not in _REDIRECT_STATUS_CODES:
|
|
111
|
+
return response
|
|
112
|
+
|
|
113
|
+
location = str(response.headers.get("location") or "").strip()
|
|
114
|
+
if not location:
|
|
115
|
+
raise SafeHttpError(
|
|
116
|
+
f"Redirect response {response.status_code} missing Location header.",
|
|
117
|
+
recoverable=True,
|
|
118
|
+
)
|
|
119
|
+
if redirects_seen >= max_redirects:
|
|
120
|
+
raise SafeHttpError(f"Too many redirects (>{max_redirects}).", recoverable=True)
|
|
121
|
+
redirects_seen += 1
|
|
122
|
+
current_url = urljoin(current_url, location)
|
|
123
|
+
if response.status_code == 303 or (
|
|
124
|
+
response.status_code in {301, 302} and current_method not in {"GET", "HEAD"}
|
|
125
|
+
):
|
|
126
|
+
current_method = "GET"
|
|
127
|
+
request_json = None
|
|
128
|
+
request_content = None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _headers_with_default_accept_encoding(headers: dict[str, str] | None) -> dict[str, str]:
|
|
132
|
+
request_headers = dict(headers or {})
|
|
133
|
+
if not any(key.lower() == "accept-encoding" for key in request_headers):
|
|
134
|
+
request_headers["Accept-Encoding"] = _DEFAULT_ACCEPT_ENCODING
|
|
135
|
+
return request_headers
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
async def _single_request(
|
|
139
|
+
*,
|
|
140
|
+
client: httpx.AsyncClient,
|
|
141
|
+
method: str,
|
|
142
|
+
target: _SafeTarget,
|
|
143
|
+
headers: dict[str, str],
|
|
144
|
+
json: Any,
|
|
145
|
+
content: bytes | None,
|
|
146
|
+
max_bytes: int,
|
|
147
|
+
) -> httpx.Response:
|
|
148
|
+
outbound_headers = dict(headers)
|
|
149
|
+
outbound_headers["Host"] = target.host_header
|
|
150
|
+
request = httpx.Request(method, target.request_url, headers=outbound_headers)
|
|
151
|
+
body = bytearray()
|
|
152
|
+
response_headers: httpx.Headers | None = None
|
|
153
|
+
status_code = 0
|
|
154
|
+
extensions: dict[str, Any] = {}
|
|
155
|
+
async with client.stream(
|
|
156
|
+
method,
|
|
157
|
+
target.connect_url,
|
|
158
|
+
headers=outbound_headers,
|
|
159
|
+
json=json,
|
|
160
|
+
content=content,
|
|
161
|
+
) as response:
|
|
162
|
+
status_code = int(response.status_code)
|
|
163
|
+
response_headers = httpx.Headers(response.headers)
|
|
164
|
+
extensions = dict(response.extensions)
|
|
165
|
+
async for chunk in response.aiter_bytes():
|
|
166
|
+
if not chunk:
|
|
167
|
+
continue
|
|
168
|
+
if len(body) + len(chunk) > max_bytes:
|
|
169
|
+
raise SafeHttpError(
|
|
170
|
+
f"Response body exceeded max_bytes={max_bytes}.", recoverable=True
|
|
171
|
+
)
|
|
172
|
+
body.extend(chunk)
|
|
173
|
+
return httpx.Response(
|
|
174
|
+
status_code=status_code,
|
|
175
|
+
headers=response_headers,
|
|
176
|
+
content=bytes(body),
|
|
177
|
+
request=request,
|
|
178
|
+
extensions=extensions,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _validate_and_resolve_target(
|
|
183
|
+
url: str,
|
|
184
|
+
*,
|
|
185
|
+
resolver: Resolver,
|
|
186
|
+
use_resolved_connect_url: bool,
|
|
187
|
+
) -> _SafeTarget:
|
|
188
|
+
split = urlsplit(str(url or "").strip())
|
|
189
|
+
scheme = split.scheme.lower()
|
|
190
|
+
if scheme not in _ALLOWED_SCHEMES:
|
|
191
|
+
raise SafeHttpError(
|
|
192
|
+
f"Unsupported URL scheme: {split.scheme or '(missing)'}", recoverable=True
|
|
193
|
+
)
|
|
194
|
+
if split.hostname is None:
|
|
195
|
+
raise SafeHttpError("URL must include a hostname.", recoverable=True)
|
|
196
|
+
if split.username is not None or split.password is not None:
|
|
197
|
+
raise SafeHttpError("Embedded URL credentials are not allowed.", recoverable=True)
|
|
198
|
+
try:
|
|
199
|
+
port = split.port
|
|
200
|
+
except ValueError as exc:
|
|
201
|
+
raise SafeHttpError("URL has an invalid port value.", recoverable=True) from exc
|
|
202
|
+
if port is None:
|
|
203
|
+
port = 443 if scheme == "https" else 80
|
|
204
|
+
|
|
205
|
+
host = split.hostname
|
|
206
|
+
try:
|
|
207
|
+
literal_address = ipaddress.ip_address(host)
|
|
208
|
+
except ValueError:
|
|
209
|
+
literal_address = None
|
|
210
|
+
if literal_address is not None:
|
|
211
|
+
reason = _blocked_ip_reason(literal_address)
|
|
212
|
+
if reason is not None:
|
|
213
|
+
raise SafeHttpError(
|
|
214
|
+
f"Blocked URL host '{host}': resolved to denied address(es): "
|
|
215
|
+
f"{literal_address.compressed} ({reason}).",
|
|
216
|
+
recoverable=True,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
addresses = resolver(host, port)
|
|
220
|
+
parsed_addresses = [_parse_ip_address(address, host=host) for address in addresses]
|
|
221
|
+
blocked = [
|
|
222
|
+
f"{address.compressed} ({_blocked_ip_reason(address)})"
|
|
223
|
+
for address in parsed_addresses
|
|
224
|
+
if _blocked_ip_reason(address) is not None
|
|
225
|
+
]
|
|
226
|
+
if blocked:
|
|
227
|
+
raise SafeHttpError(
|
|
228
|
+
f"Blocked URL host '{host}': resolved to denied address(es): {', '.join(blocked)}.",
|
|
229
|
+
recoverable=True,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
host_header = _host_header(host=host, port=split.port, scheme=scheme)
|
|
233
|
+
request_url = urlunsplit((split.scheme, split.netloc, split.path, split.query, ""))
|
|
234
|
+
if use_resolved_connect_url and scheme != "https":
|
|
235
|
+
connect_ip = parsed_addresses[0].compressed
|
|
236
|
+
connect_host = f"[{connect_ip}]" if ":" in connect_ip else connect_ip
|
|
237
|
+
connect_netloc = _host_header(host=connect_host, port=port, scheme=scheme, force_port=True)
|
|
238
|
+
connect_url = urlunsplit((split.scheme, connect_netloc, split.path, split.query, ""))
|
|
239
|
+
else:
|
|
240
|
+
# HTTPS must keep the original hostname for SNI and certificate validation.
|
|
241
|
+
# The DNS result is still preflight-validated above for SSRF-denied ranges.
|
|
242
|
+
connect_url = request_url
|
|
243
|
+
return _SafeTarget(
|
|
244
|
+
request_url=request_url,
|
|
245
|
+
connect_url=connect_url,
|
|
246
|
+
host_header=host_header,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _resolve_host_addresses(host: str, port: int) -> list[str]:
|
|
251
|
+
try:
|
|
252
|
+
infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
|
|
253
|
+
except OSError as exc:
|
|
254
|
+
raise SafeHttpError(f"Failed to resolve host '{host}': {exc}") from exc
|
|
255
|
+
addresses: list[str] = []
|
|
256
|
+
seen: set[str] = set()
|
|
257
|
+
for _family, _socktype, _proto, _canonname, sockaddr in infos:
|
|
258
|
+
if not sockaddr:
|
|
259
|
+
continue
|
|
260
|
+
address = str(sockaddr[0] or "").strip()
|
|
261
|
+
if address and address not in seen:
|
|
262
|
+
seen.add(address)
|
|
263
|
+
addresses.append(address)
|
|
264
|
+
if not addresses:
|
|
265
|
+
raise SafeHttpError(f"Host '{host}' did not resolve to any address.", recoverable=True)
|
|
266
|
+
return addresses
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _parse_ip_address(value: str, *, host: str) -> ipaddress._BaseAddress:
|
|
270
|
+
try:
|
|
271
|
+
return ipaddress.ip_address(value)
|
|
272
|
+
except ValueError as exc:
|
|
273
|
+
raise SafeHttpError(
|
|
274
|
+
f"Resolver returned a non-IP address for host '{host}': {value!r}",
|
|
275
|
+
recoverable=True,
|
|
276
|
+
) from exc
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _blocked_ip_reason(address: ipaddress._BaseAddress) -> str | None:
|
|
280
|
+
for network in _DENIED_NETWORKS:
|
|
281
|
+
if address.version == network.version and address in network:
|
|
282
|
+
return str(network)
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _host_header(
|
|
287
|
+
*,
|
|
288
|
+
host: str,
|
|
289
|
+
port: int | None,
|
|
290
|
+
scheme: str,
|
|
291
|
+
force_port: bool = False,
|
|
292
|
+
) -> str:
|
|
293
|
+
bracketed_host = f"[{host}]" if ":" in host and not host.startswith("[") else host
|
|
294
|
+
default_port = 443 if scheme == "https" else 80
|
|
295
|
+
if port is None or (port == default_port and not force_port):
|
|
296
|
+
return bracketed_host
|
|
297
|
+
return f"{bracketed_host}:{port}"
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
_MAX_REPORTED_TAGS = 16
|
|
8
|
+
_MAX_REPORTED_TAG_CHARS = 64
|
|
9
|
+
|
|
10
|
+
_ROLE_TAGS = frozenset({"assistant", "developer", "function", "system", "tool", "user"})
|
|
11
|
+
_HARNESS_TAGS = frozenset(
|
|
12
|
+
{
|
|
13
|
+
"asset_content",
|
|
14
|
+
"asset_summary",
|
|
15
|
+
"asset_text",
|
|
16
|
+
"assistant_summary",
|
|
17
|
+
"available_tool_catalog",
|
|
18
|
+
"environment_context",
|
|
19
|
+
"explicit_skill_context",
|
|
20
|
+
"matched_skill_context",
|
|
21
|
+
"ocr_text",
|
|
22
|
+
"repo_conventions",
|
|
23
|
+
"resume_context",
|
|
24
|
+
"scoped_prompt_prelude",
|
|
25
|
+
"skill_context",
|
|
26
|
+
"skill_instructions",
|
|
27
|
+
"subagent_context",
|
|
28
|
+
"subagent_turn_context",
|
|
29
|
+
"task_brief",
|
|
30
|
+
"tool_call",
|
|
31
|
+
"workspace_binding_context",
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
_TAG_RE = re.compile(
|
|
36
|
+
r"<\s*/?\s*(?P<name>[A-Za-z][A-Za-z0-9:_-]*)\b[^<>]*>",
|
|
37
|
+
flags=re.IGNORECASE,
|
|
38
|
+
)
|
|
39
|
+
_ESCAPED_TAG_RE = re.compile(
|
|
40
|
+
r"<\s*/?\s*(?P<name>[A-Za-z][A-Za-z0-9:_-]*)\b.*?>",
|
|
41
|
+
flags=re.IGNORECASE,
|
|
42
|
+
)
|
|
43
|
+
_BLOCKED_INSTRUCTION_MARKER_RE = re.compile(
|
|
44
|
+
r"\[blocked untrusted subagent "
|
|
45
|
+
r"(?:instruction_override|permission_override|tool_demand)\]",
|
|
46
|
+
flags=re.IGNORECASE,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
_HIGH_RISK_PATTERNS: tuple[tuple[str, tuple[re.Pattern[str], ...]], ...] = (
|
|
50
|
+
(
|
|
51
|
+
"instruction_override",
|
|
52
|
+
(
|
|
53
|
+
re.compile(
|
|
54
|
+
r"\b(?:ignore|disregard|forget|override|bypass)\s+"
|
|
55
|
+
r"(?:(?:all|any|the|your)\s+)?"
|
|
56
|
+
r"(?:previous|prior|above|earlier|system|developer|parent)\s+"
|
|
57
|
+
r"(?:instructions?|rules?|directives?|prompts?)\b",
|
|
58
|
+
flags=re.IGNORECASE,
|
|
59
|
+
),
|
|
60
|
+
re.compile(
|
|
61
|
+
r"\b(?:follow|obey|use)\s+(?:these|the\s+following|my|new)\s+"
|
|
62
|
+
r"instructions?\s+(?:instead|now)\b",
|
|
63
|
+
flags=re.IGNORECASE,
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
(
|
|
68
|
+
"permission_override",
|
|
69
|
+
(
|
|
70
|
+
re.compile(
|
|
71
|
+
r"\b(?:grant|escalate|elevate|change|set|switch|override|enable|disable|bypass)\s+"
|
|
72
|
+
r"(?:(?:the|your|parent|session)\s+)?"
|
|
73
|
+
r"(?:permissions?|permission\s+mode|sandbox(?:\s+mode)?|approval(?:\s+mode)?)"
|
|
74
|
+
r"(?:\s+(?:to|as)\s+[A-Za-z0-9_-]+)?\b",
|
|
75
|
+
flags=re.IGNORECASE,
|
|
76
|
+
),
|
|
77
|
+
re.compile(
|
|
78
|
+
r"\b(?:you|the\s+parent|parent|assistant|main\s+agent)\s+(?:now\s+)?"
|
|
79
|
+
r"(?:have|has)\s+(?:full|unrestricted|elevated)\s+"
|
|
80
|
+
r"(?:access|permissions?)\b",
|
|
81
|
+
flags=re.IGNORECASE,
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
(
|
|
86
|
+
"tool_demand",
|
|
87
|
+
(
|
|
88
|
+
re.compile(
|
|
89
|
+
r"\b(?:you|the\s+parent|parent|assistant|main\s+agent)\s+"
|
|
90
|
+
r"(?:must|should|shall|need(?:s)?\s+to)\s+(?:now\s+)?"
|
|
91
|
+
r"(?:call|run|invoke|use|execute)\s+(?:the\s+)?"
|
|
92
|
+
r"(?:[A-Za-z][A-Za-z0-9.-]*_[A-Za-z0-9_.-]+|"
|
|
93
|
+
r"[A-Za-z][A-Za-z0-9_.-]*\s+tool)\b",
|
|
94
|
+
flags=re.IGNORECASE,
|
|
95
|
+
),
|
|
96
|
+
re.compile(
|
|
97
|
+
r"\b(?:call|run|invoke|use|execute)\s+(?:the\s+)?"
|
|
98
|
+
r"(?:[A-Za-z][A-Za-z0-9.-]*_[A-Za-z0-9_.-]+|"
|
|
99
|
+
r"[A-Za-z][A-Za-z0-9_.-]*\s+tool)\s+(?:now|instead)\b",
|
|
100
|
+
flags=re.IGNORECASE,
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class SanitizedSubagentReport:
|
|
109
|
+
text: str
|
|
110
|
+
sanitized: bool
|
|
111
|
+
detected_categories: tuple[str, ...] = ()
|
|
112
|
+
detected_tags: tuple[str, ...] = ()
|
|
113
|
+
|
|
114
|
+
def metadata(self) -> dict[str, Any]:
|
|
115
|
+
return {
|
|
116
|
+
"sanitized": self.sanitized,
|
|
117
|
+
"detected_categories": list(self.detected_categories),
|
|
118
|
+
"detected_tags": list(self.detected_tags),
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _tag_category(tag_name: str) -> str | None:
|
|
123
|
+
normalized = tag_name.casefold()
|
|
124
|
+
if normalized in _ROLE_TAGS:
|
|
125
|
+
return "role_tag"
|
|
126
|
+
if (
|
|
127
|
+
normalized in _HARNESS_TAGS
|
|
128
|
+
or normalized.endswith("_context")
|
|
129
|
+
or normalized.startswith("alysis_")
|
|
130
|
+
):
|
|
131
|
+
return "harness_tag"
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def sanitize_subagent_report(text: str) -> SanitizedSubagentReport:
|
|
136
|
+
original = str(text or "")
|
|
137
|
+
categories: set[str] = set()
|
|
138
|
+
tags: set[str] = set()
|
|
139
|
+
|
|
140
|
+
def _escape_tag(match: re.Match[str]) -> str:
|
|
141
|
+
tag_name = str(match.group("name") or "")
|
|
142
|
+
category = _tag_category(tag_name)
|
|
143
|
+
if category is None:
|
|
144
|
+
return match.group(0)
|
|
145
|
+
categories.add(category)
|
|
146
|
+
tags.add(tag_name.casefold()[:_MAX_REPORTED_TAG_CHARS])
|
|
147
|
+
return match.group(0).replace("<", "<").replace(">", ">")
|
|
148
|
+
|
|
149
|
+
sanitized_text = _TAG_RE.sub(_escape_tag, original)
|
|
150
|
+
for category, patterns in _HIGH_RISK_PATTERNS:
|
|
151
|
+
for pattern in patterns:
|
|
152
|
+
sanitized_text, replacements = pattern.subn(
|
|
153
|
+
f"[blocked untrusted subagent {category}]",
|
|
154
|
+
sanitized_text,
|
|
155
|
+
)
|
|
156
|
+
if replacements:
|
|
157
|
+
categories.add(category)
|
|
158
|
+
|
|
159
|
+
ordered_categories = tuple(
|
|
160
|
+
category
|
|
161
|
+
for category in (
|
|
162
|
+
"role_tag",
|
|
163
|
+
"harness_tag",
|
|
164
|
+
"instruction_override",
|
|
165
|
+
"permission_override",
|
|
166
|
+
"tool_demand",
|
|
167
|
+
)
|
|
168
|
+
if category in categories
|
|
169
|
+
)
|
|
170
|
+
ordered_tags = tuple(sorted(tags)[:_MAX_REPORTED_TAGS])
|
|
171
|
+
return SanitizedSubagentReport(
|
|
172
|
+
text=sanitized_text,
|
|
173
|
+
sanitized=sanitized_text != original,
|
|
174
|
+
detected_categories=ordered_categories,
|
|
175
|
+
detected_tags=ordered_tags,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def subagent_report_evidence_text(text: str) -> str:
|
|
180
|
+
def _remove_protected_wrapper(match: re.Match[str]) -> str:
|
|
181
|
+
return " " if _tag_category(str(match.group("name") or "")) is not None else match.group(0)
|
|
182
|
+
|
|
183
|
+
without_wrappers = _ESCAPED_TAG_RE.sub(_remove_protected_wrapper, str(text or ""))
|
|
184
|
+
return _BLOCKED_INSTRUCTION_MARKER_RE.sub(" ", without_wrappers)
|