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,629 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from collections.abc import Collection
|
|
7
|
+
from dataclasses import dataclass, fields, is_dataclass, replace
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from ..tools.fs import classify_sensitive_path
|
|
11
|
+
|
|
12
|
+
_SENSITIVE_POLICY_KEY = "_alysis_output_policy"
|
|
13
|
+
_REDACTED_ARGUMENT = "[redacted: sensitive file content]"
|
|
14
|
+
_REDACTED_MESSAGE = "Sensitive tool output redacted."
|
|
15
|
+
_REDACTED_RESPONSE_TAINT = "[redacted: approved sensitive content]"
|
|
16
|
+
_TERMINAL_SENSITIVE_READ_ERROR = (
|
|
17
|
+
"Sensitive path is protected and will not be readable after this failure. "
|
|
18
|
+
"No content was returned. This failure is terminal; do not retry."
|
|
19
|
+
)
|
|
20
|
+
_CONTENT_ARGUMENT_KEYS = frozenset(
|
|
21
|
+
{
|
|
22
|
+
"body",
|
|
23
|
+
"bytes",
|
|
24
|
+
"content",
|
|
25
|
+
"contents",
|
|
26
|
+
"data",
|
|
27
|
+
"diff",
|
|
28
|
+
"edit",
|
|
29
|
+
"edits",
|
|
30
|
+
"expected",
|
|
31
|
+
"expected_old",
|
|
32
|
+
"insert",
|
|
33
|
+
"lines",
|
|
34
|
+
"new",
|
|
35
|
+
"new_content",
|
|
36
|
+
"old",
|
|
37
|
+
"old_content",
|
|
38
|
+
"patch",
|
|
39
|
+
"replacement",
|
|
40
|
+
"text",
|
|
41
|
+
"value",
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
_DIRECT_PATH_FIELDS = (
|
|
45
|
+
"path",
|
|
46
|
+
"source_path",
|
|
47
|
+
"destination_path",
|
|
48
|
+
"file",
|
|
49
|
+
"file_path",
|
|
50
|
+
)
|
|
51
|
+
_PATCH_PATH_RE = re.compile(r"^(?:\+\+\+|---)\s+(?:[ab]/)?([^\t\r\n]+)", re.MULTILINE)
|
|
52
|
+
_TAINT_VALUE_KEYS = _CONTENT_ARGUMENT_KEYS | frozenset({"insert_content", "replacement", "target"})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, slots=True)
|
|
56
|
+
class SensitiveToolBoundary:
|
|
57
|
+
"""Persistence/display policy derived without opening sensitive files."""
|
|
58
|
+
|
|
59
|
+
sensitive: bool = False
|
|
60
|
+
paths: tuple[str, ...] = ()
|
|
61
|
+
categories: tuple[str, ...] = ()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class _PersistenceSafeToolCall:
|
|
66
|
+
id: str
|
|
67
|
+
name: str
|
|
68
|
+
arguments: Any
|
|
69
|
+
provider_metadata: Any = None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True, slots=True)
|
|
73
|
+
class _PersistenceSafeResponse:
|
|
74
|
+
content: str
|
|
75
|
+
tool_calls: list[Any]
|
|
76
|
+
raw: dict[str, Any]
|
|
77
|
+
response_model: Any = None
|
|
78
|
+
usage: Any = None
|
|
79
|
+
provider_metadata: Any = None
|
|
80
|
+
reasoning: Any = ()
|
|
81
|
+
assistant_phase: Any = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _normalized_unique_strings(values: list[Any]) -> tuple[str, ...]:
|
|
85
|
+
normalized: list[str] = []
|
|
86
|
+
seen: set[str] = set()
|
|
87
|
+
for value in values:
|
|
88
|
+
item = str(value or "").strip().replace("\\", "/")
|
|
89
|
+
if not item or item == "/dev/null":
|
|
90
|
+
continue
|
|
91
|
+
key = item.casefold()
|
|
92
|
+
if key in seen:
|
|
93
|
+
continue
|
|
94
|
+
seen.add(key)
|
|
95
|
+
normalized.append(item)
|
|
96
|
+
return tuple(normalized)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _candidate_paths(tool_name: str, arguments: Any) -> tuple[str, ...]:
|
|
100
|
+
if not isinstance(arguments, dict):
|
|
101
|
+
return ()
|
|
102
|
+
normalized_name = str(tool_name or "").strip().casefold()
|
|
103
|
+
if not (normalized_name.startswith("fs_") or normalized_name == "git_apply_patch"):
|
|
104
|
+
return ()
|
|
105
|
+
candidates = [arguments.get(field) for field in _DIRECT_PATH_FIELDS]
|
|
106
|
+
if normalized_name == "git_apply_patch":
|
|
107
|
+
patch = arguments.get("patch")
|
|
108
|
+
if patch is None:
|
|
109
|
+
patch = arguments.get("diff")
|
|
110
|
+
if patch is not None:
|
|
111
|
+
candidates.extend(_PATCH_PATH_RE.findall(str(patch)))
|
|
112
|
+
return _normalized_unique_strings(candidates)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _policy_categories(result: Any) -> tuple[str, ...]:
|
|
116
|
+
if not isinstance(result, dict):
|
|
117
|
+
return ()
|
|
118
|
+
policy = result.get(_SENSITIVE_POLICY_KEY)
|
|
119
|
+
if not isinstance(policy, dict) or policy.get("sensitive") is not True:
|
|
120
|
+
return ()
|
|
121
|
+
if str(policy.get("persist") or "").casefold() != "redact":
|
|
122
|
+
return ()
|
|
123
|
+
if str(policy.get("display") or "").casefold() != "redact":
|
|
124
|
+
return ()
|
|
125
|
+
raw_categories = policy.get("categories")
|
|
126
|
+
if not isinstance(raw_categories, list | tuple):
|
|
127
|
+
return ()
|
|
128
|
+
return _normalized_unique_strings(list(raw_categories))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def sensitive_tool_boundary(
|
|
132
|
+
tool_name: str,
|
|
133
|
+
arguments: Any,
|
|
134
|
+
*,
|
|
135
|
+
result: Any = None,
|
|
136
|
+
) -> SensitiveToolBoundary:
|
|
137
|
+
paths = _candidate_paths(tool_name, arguments)
|
|
138
|
+
categories: list[str] = []
|
|
139
|
+
sensitive_paths: list[str] = []
|
|
140
|
+
for path in paths:
|
|
141
|
+
classification = classify_sensitive_path(path)
|
|
142
|
+
if not classification.sensitive:
|
|
143
|
+
continue
|
|
144
|
+
sensitive_paths.append(path)
|
|
145
|
+
categories.append(str(classification.category or "sensitive_file"))
|
|
146
|
+
|
|
147
|
+
policy_categories = _policy_categories(result)
|
|
148
|
+
policy = result.get(_SENSITIVE_POLICY_KEY) if isinstance(result, dict) else None
|
|
149
|
+
policy_sensitive = isinstance(policy, dict) and policy.get("sensitive") is True
|
|
150
|
+
if not sensitive_paths and not policy_sensitive:
|
|
151
|
+
return SensitiveToolBoundary()
|
|
152
|
+
return SensitiveToolBoundary(
|
|
153
|
+
sensitive=True,
|
|
154
|
+
paths=_normalized_unique_strings(sensitive_paths or list(paths)),
|
|
155
|
+
categories=_normalized_unique_strings(categories + list(policy_categories)),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _redact_content_fields(value: Any, *, parent_key: str = "") -> Any:
|
|
160
|
+
if parent_key.casefold() in _CONTENT_ARGUMENT_KEYS:
|
|
161
|
+
return _REDACTED_ARGUMENT
|
|
162
|
+
if isinstance(value, dict):
|
|
163
|
+
return {
|
|
164
|
+
str(key): _redact_content_fields(item, parent_key=str(key))
|
|
165
|
+
for key, item in value.items()
|
|
166
|
+
}
|
|
167
|
+
if isinstance(value, list):
|
|
168
|
+
return [_redact_content_fields(item) for item in value]
|
|
169
|
+
if isinstance(value, tuple):
|
|
170
|
+
return tuple(_redact_content_fields(item) for item in value)
|
|
171
|
+
return copy.deepcopy(value)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def redact_sensitive_tool_arguments(
|
|
175
|
+
tool_name: str,
|
|
176
|
+
arguments: Any,
|
|
177
|
+
*,
|
|
178
|
+
boundary: SensitiveToolBoundary | None = None,
|
|
179
|
+
) -> Any:
|
|
180
|
+
effective = boundary or sensitive_tool_boundary(tool_name, arguments)
|
|
181
|
+
if not effective.sensitive:
|
|
182
|
+
return arguments
|
|
183
|
+
return _redact_content_fields(arguments)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def sensitive_result_stub(
|
|
187
|
+
tool_name: str,
|
|
188
|
+
boundary: SensitiveToolBoundary,
|
|
189
|
+
) -> dict[str, Any]:
|
|
190
|
+
"""Return the only representation allowed beyond the immediate model call."""
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
"_alysis_redacted": True,
|
|
194
|
+
"sensitive": True,
|
|
195
|
+
"persist": "redact",
|
|
196
|
+
"display": "redact",
|
|
197
|
+
"tool": str(tool_name or ""),
|
|
198
|
+
"paths": list(boundary.paths),
|
|
199
|
+
"categories": list(boundary.categories),
|
|
200
|
+
"message": _REDACTED_MESSAGE,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def redact_sensitive_tool_result(
|
|
205
|
+
tool_name: str,
|
|
206
|
+
arguments: Any,
|
|
207
|
+
result: Any,
|
|
208
|
+
*,
|
|
209
|
+
boundary: SensitiveToolBoundary | None = None,
|
|
210
|
+
) -> Any:
|
|
211
|
+
effective = boundary or sensitive_tool_boundary(tool_name, arguments, result=result)
|
|
212
|
+
if not effective.sensitive:
|
|
213
|
+
return result
|
|
214
|
+
stub = sensitive_result_stub(tool_name, effective)
|
|
215
|
+
if isinstance(result, dict) and "error" in result:
|
|
216
|
+
normalized_tool = str(tool_name or "").strip().casefold()
|
|
217
|
+
if normalized_tool in {"fs_read", "fs_read_lines"}:
|
|
218
|
+
if result.get("error_code") == "fs_path_not_found":
|
|
219
|
+
path = effective.paths[0] if effective.paths else "requested path"
|
|
220
|
+
stub["error"] = (
|
|
221
|
+
f"Path does not exist: {path}. This result is terminal; do not retry this path."
|
|
222
|
+
)
|
|
223
|
+
stub["error_code"] = "fs_path_not_found"
|
|
224
|
+
else:
|
|
225
|
+
stub["error"] = _TERMINAL_SENSITIVE_READ_ERROR
|
|
226
|
+
stub["error_code"] = "sensitive_read_terminal"
|
|
227
|
+
stub["terminal"] = True
|
|
228
|
+
stub["retryable"] = False
|
|
229
|
+
else:
|
|
230
|
+
stub["error"] = "Sensitive tool operation failed; details redacted."
|
|
231
|
+
return stub
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _add_taint_variants(values: set[str], raw_value: Any) -> None:
|
|
235
|
+
if isinstance(raw_value, bytes):
|
|
236
|
+
text = raw_value.decode("utf-8", errors="replace")
|
|
237
|
+
elif isinstance(raw_value, str):
|
|
238
|
+
text = raw_value
|
|
239
|
+
else:
|
|
240
|
+
return
|
|
241
|
+
if not text or not text.strip():
|
|
242
|
+
return
|
|
243
|
+
values.add(text)
|
|
244
|
+
stripped = text.strip()
|
|
245
|
+
if stripped:
|
|
246
|
+
values.add(stripped)
|
|
247
|
+
escaped = json.dumps(text, ensure_ascii=False)[1:-1]
|
|
248
|
+
if escaped and escaped != text:
|
|
249
|
+
values.add(escaped)
|
|
250
|
+
|
|
251
|
+
def _collect_json_strings(value: Any) -> None:
|
|
252
|
+
if isinstance(value, str):
|
|
253
|
+
if value.strip():
|
|
254
|
+
values.add(value)
|
|
255
|
+
return
|
|
256
|
+
if isinstance(value, dict):
|
|
257
|
+
for item in value.values():
|
|
258
|
+
_collect_json_strings(item)
|
|
259
|
+
return
|
|
260
|
+
if isinstance(value, list):
|
|
261
|
+
for item in value:
|
|
262
|
+
_collect_json_strings(item)
|
|
263
|
+
|
|
264
|
+
def _collect_json_text(candidate: str) -> None:
|
|
265
|
+
try:
|
|
266
|
+
decoded = json.loads(candidate)
|
|
267
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
268
|
+
return
|
|
269
|
+
_collect_json_strings(decoded)
|
|
270
|
+
|
|
271
|
+
for line in text.splitlines():
|
|
272
|
+
line_value = line.strip()
|
|
273
|
+
if line_value:
|
|
274
|
+
values.add(line_value)
|
|
275
|
+
for separator in ("=", ":"):
|
|
276
|
+
if separator not in line_value:
|
|
277
|
+
continue
|
|
278
|
+
key, candidate = line_value.split(separator, 1)
|
|
279
|
+
if not key.strip() or not candidate.strip():
|
|
280
|
+
continue
|
|
281
|
+
candidate = candidate.strip().strip("\"'")
|
|
282
|
+
if candidate:
|
|
283
|
+
values.add(candidate)
|
|
284
|
+
_collect_json_text(candidate)
|
|
285
|
+
break
|
|
286
|
+
_collect_json_text(text)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _collect_taint_values(value: Any, values: set[str], *, parent_key: str = "") -> None:
|
|
290
|
+
if isinstance(value, dict):
|
|
291
|
+
for key, item in value.items():
|
|
292
|
+
normalized_key = str(key).casefold()
|
|
293
|
+
if normalized_key in _TAINT_VALUE_KEYS and isinstance(item, str | bytes):
|
|
294
|
+
_add_taint_variants(values, item)
|
|
295
|
+
else:
|
|
296
|
+
_collect_taint_values(item, values, parent_key=normalized_key)
|
|
297
|
+
return
|
|
298
|
+
if isinstance(value, list | tuple):
|
|
299
|
+
for item in value:
|
|
300
|
+
_collect_taint_values(item, values, parent_key=parent_key)
|
|
301
|
+
return
|
|
302
|
+
if parent_key in _TAINT_VALUE_KEYS:
|
|
303
|
+
_add_taint_variants(values, value)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def collect_sensitive_response_taints(
|
|
307
|
+
tool_name: str,
|
|
308
|
+
arguments: Any,
|
|
309
|
+
result: Any,
|
|
310
|
+
*,
|
|
311
|
+
boundary: SensitiveToolBoundary | None = None,
|
|
312
|
+
) -> tuple[str, ...]:
|
|
313
|
+
"""Collect exact approved content values that a provider must not echo durably.
|
|
314
|
+
|
|
315
|
+
Paths, categories, operation names, and unrelated metadata are deliberately
|
|
316
|
+
excluded. Only content-bearing leaves from a sensitive tool exchange become
|
|
317
|
+
taints, which keeps redaction narrow and value-specific.
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
effective = boundary or sensitive_tool_boundary(tool_name, arguments, result=result)
|
|
321
|
+
if not effective.sensitive:
|
|
322
|
+
return ()
|
|
323
|
+
values: set[str] = set()
|
|
324
|
+
_collect_taint_values(arguments, values)
|
|
325
|
+
if not (isinstance(result, dict) and "error" in result):
|
|
326
|
+
if isinstance(result, str | bytes):
|
|
327
|
+
_add_taint_variants(values, result)
|
|
328
|
+
else:
|
|
329
|
+
_collect_taint_values(result, values)
|
|
330
|
+
return tuple(sorted(values, key=lambda item: (-len(item), item)))
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _redact_tainted_text(text: str, taints: tuple[str, ...]) -> str:
|
|
334
|
+
redacted = text
|
|
335
|
+
for taint in taints:
|
|
336
|
+
if taint and taint in redacted:
|
|
337
|
+
redacted = redacted.replace(taint, _REDACTED_RESPONSE_TAINT)
|
|
338
|
+
return redacted
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _redact_tainted_value(value: Any, taints: tuple[str, ...]) -> Any:
|
|
342
|
+
if isinstance(value, str):
|
|
343
|
+
redacted = _redact_tainted_text(value, taints)
|
|
344
|
+
return value if redacted == value else redacted
|
|
345
|
+
if isinstance(value, bytes):
|
|
346
|
+
decoded = value.decode("utf-8", errors="replace")
|
|
347
|
+
redacted = _redact_tainted_text(decoded, taints)
|
|
348
|
+
return value if redacted == decoded else redacted.encode("utf-8")
|
|
349
|
+
if isinstance(value, dict):
|
|
350
|
+
copied: dict[Any, Any] | None = None
|
|
351
|
+
for key, item in value.items():
|
|
352
|
+
redacted_item = _redact_tainted_value(item, taints)
|
|
353
|
+
if redacted_item is not item and copied is None:
|
|
354
|
+
copied = dict(value)
|
|
355
|
+
if copied is not None:
|
|
356
|
+
copied[key] = redacted_item
|
|
357
|
+
return value if copied is None else copied
|
|
358
|
+
if isinstance(value, list):
|
|
359
|
+
redacted_items = [_redact_tainted_value(item, taints) for item in value]
|
|
360
|
+
if all(
|
|
361
|
+
redacted is original for redacted, original in zip(redacted_items, value, strict=True)
|
|
362
|
+
):
|
|
363
|
+
return value
|
|
364
|
+
return redacted_items
|
|
365
|
+
if isinstance(value, tuple):
|
|
366
|
+
redacted_items = tuple(_redact_tainted_value(item, taints) for item in value)
|
|
367
|
+
if all(
|
|
368
|
+
redacted is original for redacted, original in zip(redacted_items, value, strict=True)
|
|
369
|
+
):
|
|
370
|
+
return value
|
|
371
|
+
return redacted_items
|
|
372
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
373
|
+
updates: dict[str, Any] = {}
|
|
374
|
+
for dataclass_field in fields(value):
|
|
375
|
+
original = getattr(value, dataclass_field.name)
|
|
376
|
+
redacted = _redact_tainted_value(original, taints)
|
|
377
|
+
if redacted is not original:
|
|
378
|
+
updates[dataclass_field.name] = redacted
|
|
379
|
+
if not updates:
|
|
380
|
+
return value
|
|
381
|
+
try:
|
|
382
|
+
return replace(value, **updates)
|
|
383
|
+
except (TypeError, ValueError):
|
|
384
|
+
return value
|
|
385
|
+
return value
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def redact_sensitive_response_taints(response: Any, taints: Collection[str]) -> Any:
|
|
389
|
+
"""Redact exact approved values from every provider-response channel."""
|
|
390
|
+
|
|
391
|
+
normalized = tuple(
|
|
392
|
+
sorted(
|
|
393
|
+
{str(value) for value in taints if isinstance(value, str) and value},
|
|
394
|
+
key=lambda item: (-len(item), item),
|
|
395
|
+
)
|
|
396
|
+
)
|
|
397
|
+
if not normalized:
|
|
398
|
+
return response
|
|
399
|
+
redacted_response = _redact_tainted_value(response, normalized)
|
|
400
|
+
if redacted_response is not response:
|
|
401
|
+
return redacted_response
|
|
402
|
+
|
|
403
|
+
content = _redact_tainted_value(str(getattr(response, "content", "") or ""), normalized)
|
|
404
|
+
raw = _redact_tainted_value(getattr(response, "raw", {}), normalized)
|
|
405
|
+
response_tool_calls = list(getattr(response, "tool_calls", []) or [])
|
|
406
|
+
tool_calls: list[Any] = []
|
|
407
|
+
calls_changed = False
|
|
408
|
+
for tool_call in response_tool_calls:
|
|
409
|
+
arguments = _redact_tainted_value(getattr(tool_call, "arguments", None), normalized)
|
|
410
|
+
provider_metadata = _redact_tainted_value(
|
|
411
|
+
getattr(tool_call, "provider_metadata", None), normalized
|
|
412
|
+
)
|
|
413
|
+
if arguments is getattr(tool_call, "arguments", None) and provider_metadata is getattr(
|
|
414
|
+
tool_call, "provider_metadata", None
|
|
415
|
+
):
|
|
416
|
+
tool_calls.append(tool_call)
|
|
417
|
+
continue
|
|
418
|
+
calls_changed = True
|
|
419
|
+
tool_calls.append(
|
|
420
|
+
_PersistenceSafeToolCall(
|
|
421
|
+
id=str(getattr(tool_call, "id", "") or ""),
|
|
422
|
+
name=str(getattr(tool_call, "name", "") or ""),
|
|
423
|
+
arguments=arguments,
|
|
424
|
+
provider_metadata=provider_metadata,
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
provider_metadata = _redact_tainted_value(
|
|
428
|
+
getattr(response, "provider_metadata", None), normalized
|
|
429
|
+
)
|
|
430
|
+
reasoning = _redact_tainted_value(getattr(response, "reasoning", ()), normalized)
|
|
431
|
+
usage = _redact_tainted_value(getattr(response, "usage", None), normalized)
|
|
432
|
+
changed = (
|
|
433
|
+
content != str(getattr(response, "content", "") or "")
|
|
434
|
+
or raw is not getattr(response, "raw", {})
|
|
435
|
+
or calls_changed
|
|
436
|
+
or provider_metadata is not getattr(response, "provider_metadata", None)
|
|
437
|
+
or reasoning is not getattr(response, "reasoning", ())
|
|
438
|
+
or usage is not getattr(response, "usage", None)
|
|
439
|
+
)
|
|
440
|
+
if not changed:
|
|
441
|
+
return response
|
|
442
|
+
return _PersistenceSafeResponse(
|
|
443
|
+
content=content,
|
|
444
|
+
tool_calls=tool_calls,
|
|
445
|
+
raw=raw,
|
|
446
|
+
response_model=copy.deepcopy(getattr(response, "response_model", None)),
|
|
447
|
+
usage=usage,
|
|
448
|
+
provider_metadata=provider_metadata,
|
|
449
|
+
reasoning=reasoning,
|
|
450
|
+
assistant_phase=copy.deepcopy(getattr(response, "assistant_phase", None)),
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def redact_assistant_tool_call_message(message: dict[str, Any]) -> dict[str, Any]:
|
|
455
|
+
"""Redact sensitive call bodies while preserving provider call IDs and shape."""
|
|
456
|
+
|
|
457
|
+
tool_calls = message.get("tool_calls")
|
|
458
|
+
if not isinstance(tool_calls, list):
|
|
459
|
+
return message
|
|
460
|
+
copied: dict[str, Any] | None = None
|
|
461
|
+
copied_calls: list[Any] = []
|
|
462
|
+
for call in tool_calls:
|
|
463
|
+
if not isinstance(call, dict):
|
|
464
|
+
copied_calls.append(copy.deepcopy(call))
|
|
465
|
+
continue
|
|
466
|
+
function = call.get("function")
|
|
467
|
+
if not isinstance(function, dict):
|
|
468
|
+
copied_calls.append(copy.deepcopy(call))
|
|
469
|
+
continue
|
|
470
|
+
tool_name = str(function.get("name") or "")
|
|
471
|
+
serialized = function.get("arguments")
|
|
472
|
+
try:
|
|
473
|
+
arguments = json.loads(serialized) if isinstance(serialized, str) else serialized
|
|
474
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
475
|
+
arguments = {}
|
|
476
|
+
boundary = sensitive_tool_boundary(tool_name, arguments)
|
|
477
|
+
if not boundary.sensitive:
|
|
478
|
+
copied_calls.append(copy.deepcopy(call))
|
|
479
|
+
continue
|
|
480
|
+
if copied is None:
|
|
481
|
+
copied = copy.deepcopy(message)
|
|
482
|
+
copied_call = copy.deepcopy(call)
|
|
483
|
+
copied_function = dict(copied_call["function"])
|
|
484
|
+
redacted = redact_sensitive_tool_arguments(
|
|
485
|
+
tool_name,
|
|
486
|
+
arguments,
|
|
487
|
+
boundary=boundary,
|
|
488
|
+
)
|
|
489
|
+
copied_function["arguments"] = json.dumps(redacted, ensure_ascii=False)
|
|
490
|
+
copied_call["function"] = copied_function
|
|
491
|
+
copied_calls.append(copied_call)
|
|
492
|
+
if copied is None:
|
|
493
|
+
return message
|
|
494
|
+
copied["content"] = ""
|
|
495
|
+
copied["tool_calls"] = copied_calls
|
|
496
|
+
return copied
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def redact_sensitive_response_for_persistence(
|
|
500
|
+
response: Any, *, taints: Collection[str] = ()
|
|
501
|
+
) -> Any:
|
|
502
|
+
"""Return a usage/persistence-safe response while preserving accounting fields."""
|
|
503
|
+
|
|
504
|
+
response = redact_sensitive_response_taints(response, taints)
|
|
505
|
+
|
|
506
|
+
tool_calls = getattr(response, "tool_calls", None)
|
|
507
|
+
if not isinstance(tool_calls, list):
|
|
508
|
+
return response
|
|
509
|
+
copied_calls: list[Any] = []
|
|
510
|
+
changed = False
|
|
511
|
+
for tool_call in tool_calls:
|
|
512
|
+
tool_name = str(getattr(tool_call, "name", "") or "")
|
|
513
|
+
arguments = getattr(tool_call, "arguments", None)
|
|
514
|
+
boundary = sensitive_tool_boundary(tool_name, arguments)
|
|
515
|
+
if not boundary.sensitive:
|
|
516
|
+
copied_calls.append(tool_call)
|
|
517
|
+
continue
|
|
518
|
+
changed = True
|
|
519
|
+
redacted_arguments = redact_sensitive_tool_arguments(
|
|
520
|
+
tool_name,
|
|
521
|
+
arguments,
|
|
522
|
+
boundary=boundary,
|
|
523
|
+
)
|
|
524
|
+
try:
|
|
525
|
+
copied_calls.append(replace(tool_call, arguments=redacted_arguments))
|
|
526
|
+
except (TypeError, ValueError):
|
|
527
|
+
copied_calls.append(
|
|
528
|
+
_PersistenceSafeToolCall(
|
|
529
|
+
id=str(getattr(tool_call, "id", "") or ""),
|
|
530
|
+
name=tool_name,
|
|
531
|
+
arguments=redacted_arguments,
|
|
532
|
+
provider_metadata=copy.deepcopy(getattr(tool_call, "provider_metadata", None)),
|
|
533
|
+
)
|
|
534
|
+
)
|
|
535
|
+
if not changed:
|
|
536
|
+
return response
|
|
537
|
+
try:
|
|
538
|
+
# Provider-native raw bodies commonly repeat function arguments. They
|
|
539
|
+
# are not needed for usage accounting and cannot safely be retained.
|
|
540
|
+
return replace(response, content="", tool_calls=copied_calls, raw={})
|
|
541
|
+
except (TypeError, ValueError):
|
|
542
|
+
return _PersistenceSafeResponse(
|
|
543
|
+
content="",
|
|
544
|
+
tool_calls=copied_calls,
|
|
545
|
+
raw={},
|
|
546
|
+
response_model=copy.deepcopy(getattr(response, "response_model", None)),
|
|
547
|
+
usage=copy.deepcopy(getattr(response, "usage", None)),
|
|
548
|
+
provider_metadata=copy.deepcopy(getattr(response, "provider_metadata", None)),
|
|
549
|
+
reasoning=copy.deepcopy(getattr(response, "reasoning", ())),
|
|
550
|
+
assistant_phase=copy.deepcopy(getattr(response, "assistant_phase", None)),
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def redact_consumed_sensitive_tool_messages(
|
|
555
|
+
messages: list[dict[str, Any]],
|
|
556
|
+
result_stubs: dict[str, str],
|
|
557
|
+
) -> None:
|
|
558
|
+
"""Scrub one-call-only sensitive messages after the provider consumed them."""
|
|
559
|
+
|
|
560
|
+
if not result_stubs:
|
|
561
|
+
return
|
|
562
|
+
call_ids = set(result_stubs)
|
|
563
|
+
for index, message in enumerate(messages):
|
|
564
|
+
if not isinstance(message, dict):
|
|
565
|
+
continue
|
|
566
|
+
if str(message.get("role") or "") == "tool":
|
|
567
|
+
call_id = str(message.get("tool_call_id") or "")
|
|
568
|
+
if call_id in result_stubs:
|
|
569
|
+
message["content"] = result_stubs[call_id]
|
|
570
|
+
continue
|
|
571
|
+
if str(message.get("role") or "") != "assistant":
|
|
572
|
+
continue
|
|
573
|
+
tool_calls = message.get("tool_calls")
|
|
574
|
+
if not isinstance(tool_calls, list):
|
|
575
|
+
continue
|
|
576
|
+
if any(
|
|
577
|
+
isinstance(call, dict) and str(call.get("id") or "") in call_ids for call in tool_calls
|
|
578
|
+
):
|
|
579
|
+
messages[index] = redact_assistant_tool_call_message(message)
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def inject_ephemeral_sensitive_tool_messages(
|
|
583
|
+
messages: list[dict[str, Any]],
|
|
584
|
+
*,
|
|
585
|
+
result_content: dict[str, str],
|
|
586
|
+
arguments_content: dict[str, str],
|
|
587
|
+
) -> list[dict[str, Any]]:
|
|
588
|
+
"""Expose sensitive tool material to one provider request, without mutating history."""
|
|
589
|
+
|
|
590
|
+
call_ids = set(result_content) | set(arguments_content)
|
|
591
|
+
if not call_ids:
|
|
592
|
+
return messages
|
|
593
|
+
injected = copy.deepcopy(messages)
|
|
594
|
+
for message in injected:
|
|
595
|
+
if str(message.get("role") or "") == "tool":
|
|
596
|
+
call_id = str(message.get("tool_call_id") or "")
|
|
597
|
+
if call_id in result_content:
|
|
598
|
+
message["content"] = result_content[call_id]
|
|
599
|
+
continue
|
|
600
|
+
if str(message.get("role") or "") != "assistant":
|
|
601
|
+
continue
|
|
602
|
+
tool_calls = message.get("tool_calls")
|
|
603
|
+
if not isinstance(tool_calls, list):
|
|
604
|
+
continue
|
|
605
|
+
for call in tool_calls:
|
|
606
|
+
if not isinstance(call, dict):
|
|
607
|
+
continue
|
|
608
|
+
call_id = str(call.get("id") or "")
|
|
609
|
+
if call_id not in arguments_content:
|
|
610
|
+
continue
|
|
611
|
+
function = call.get("function")
|
|
612
|
+
if isinstance(function, dict):
|
|
613
|
+
function["arguments"] = arguments_content[call_id]
|
|
614
|
+
return injected
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
__all__ = [
|
|
618
|
+
"SensitiveToolBoundary",
|
|
619
|
+
"collect_sensitive_response_taints",
|
|
620
|
+
"inject_ephemeral_sensitive_tool_messages",
|
|
621
|
+
"redact_assistant_tool_call_message",
|
|
622
|
+
"redact_consumed_sensitive_tool_messages",
|
|
623
|
+
"redact_sensitive_tool_arguments",
|
|
624
|
+
"redact_sensitive_tool_result",
|
|
625
|
+
"redact_sensitive_response_for_persistence",
|
|
626
|
+
"redact_sensitive_response_taints",
|
|
627
|
+
"sensitive_result_stub",
|
|
628
|
+
"sensitive_tool_boundary",
|
|
629
|
+
]
|