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,460 @@
|
|
|
1
|
+
"""Durable ownership records for background terminal processes.
|
|
2
|
+
|
|
3
|
+
The in-memory terminal manager can terminate everything during an orderly shutdown. These records
|
|
4
|
+
cover the harder case where the bridge is killed before teardown runs. A later Alysis Code process
|
|
5
|
+
only signals an exact PID/process-group whose creation token still matches a record written at
|
|
6
|
+
spawn time; it never enumerates or matches processes by name or command line.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import contextlib
|
|
12
|
+
import ctypes
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import signal
|
|
17
|
+
import stat
|
|
18
|
+
import subprocess
|
|
19
|
+
import time
|
|
20
|
+
import uuid
|
|
21
|
+
from ctypes import wintypes
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Literal
|
|
25
|
+
|
|
26
|
+
from .branding import canonical_user_data_dir, env_get
|
|
27
|
+
|
|
28
|
+
_SCHEMA_VERSION = 1
|
|
29
|
+
_MAX_RECORD_BYTES = 8192
|
|
30
|
+
_MAX_RECORDS = 256
|
|
31
|
+
_TERMINATION_CONFIRM_TIMEOUT_S = 1.0
|
|
32
|
+
_TERMINATION_CONFIRM_INTERVAL_S = 0.02
|
|
33
|
+
_DOCKER_CONTAINER_PATTERN = re.compile(r"^alysis-bgsbx-[0-9a-f]{12}$")
|
|
34
|
+
_ProcessState = Literal["matching", "dead", "reused", "unknown"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class TerminalOwnershipHandle:
|
|
39
|
+
record_path: Path
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class TerminalScavengeResult:
|
|
44
|
+
record: str
|
|
45
|
+
action: Literal["preserved", "terminated", "absent", "refused", "invalid"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TerminalOwnershipLedger:
|
|
49
|
+
def __init__(self, state_dir: Path | None = None) -> None:
|
|
50
|
+
configured = str(env_get("ALYSIS_TERMINAL_OWNERSHIP_DIR") or "").strip()
|
|
51
|
+
configured_data = str(env_get("ALYSIS_DATA_DIR") or "").strip()
|
|
52
|
+
data_root = (
|
|
53
|
+
Path(configured_data).expanduser() if configured_data else canonical_user_data_dir()
|
|
54
|
+
)
|
|
55
|
+
default = Path(configured).expanduser() if configured else data_root / "terminal-processes"
|
|
56
|
+
self.state_dir = (state_dir or default).expanduser().resolve()
|
|
57
|
+
|
|
58
|
+
def scavenge_stale(self) -> tuple[TerminalScavengeResult, ...]:
|
|
59
|
+
try:
|
|
60
|
+
self.state_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
61
|
+
except OSError:
|
|
62
|
+
return ()
|
|
63
|
+
results: list[TerminalScavengeResult] = []
|
|
64
|
+
try:
|
|
65
|
+
candidates = sorted(self.state_dir.glob("*.json"))[:_MAX_RECORDS]
|
|
66
|
+
except OSError:
|
|
67
|
+
return ()
|
|
68
|
+
for path in candidates:
|
|
69
|
+
payload = _read_record(path)
|
|
70
|
+
if payload is None:
|
|
71
|
+
_unlink_record(path)
|
|
72
|
+
results.append(TerminalScavengeResult(path.name, "invalid"))
|
|
73
|
+
continue
|
|
74
|
+
owner_state = _process_state(payload["owner_pid"], payload["owner_start_token"])
|
|
75
|
+
if owner_state in {"matching", "unknown"}:
|
|
76
|
+
results.append(TerminalScavengeResult(path.name, "preserved"))
|
|
77
|
+
continue
|
|
78
|
+
child_state = _process_state(payload["child_pid"], payload["child_start_token"])
|
|
79
|
+
if child_state == "matching":
|
|
80
|
+
terminated = _terminate_owned_process(payload)
|
|
81
|
+
action: TerminalScavengeResult = TerminalScavengeResult(
|
|
82
|
+
path.name, "terminated" if terminated else "refused"
|
|
83
|
+
)
|
|
84
|
+
consume_record = terminated
|
|
85
|
+
elif child_state == "dead":
|
|
86
|
+
_cleanup_owned_resource(payload)
|
|
87
|
+
action = TerminalScavengeResult(path.name, "absent")
|
|
88
|
+
consume_record = True
|
|
89
|
+
elif child_state == "reused":
|
|
90
|
+
# The recorded process no longer exists. Never signal its recycled PID, but the
|
|
91
|
+
# stale ownership record can safely be consumed.
|
|
92
|
+
action = TerminalScavengeResult(path.name, "refused")
|
|
93
|
+
consume_record = True
|
|
94
|
+
else:
|
|
95
|
+
# An unverifiable identity may still be the owned process. Keep its record so a
|
|
96
|
+
# later scavenger can retry after a transient inspection or access failure.
|
|
97
|
+
action = TerminalScavengeResult(path.name, "refused")
|
|
98
|
+
consume_record = False
|
|
99
|
+
if consume_record:
|
|
100
|
+
_unlink_record(path)
|
|
101
|
+
results.append(action)
|
|
102
|
+
return tuple(results)
|
|
103
|
+
|
|
104
|
+
def track(
|
|
105
|
+
self,
|
|
106
|
+
*,
|
|
107
|
+
child_pid: int,
|
|
108
|
+
termination_mode: str,
|
|
109
|
+
posix_pgid: int | None,
|
|
110
|
+
cleanup_kind: str | None = None,
|
|
111
|
+
cleanup_id: str | None = None,
|
|
112
|
+
) -> TerminalOwnershipHandle | None:
|
|
113
|
+
owner_pid = os.getpid()
|
|
114
|
+
owner_token = _pid_start_token(owner_pid)
|
|
115
|
+
child_token = _pid_start_token(child_pid)
|
|
116
|
+
if owner_pid <= 1 or child_pid <= 1 or owner_token is None or child_token is None:
|
|
117
|
+
return None
|
|
118
|
+
if termination_mode not in {"process_group", "direct"}:
|
|
119
|
+
return None
|
|
120
|
+
pgid: int | None = None
|
|
121
|
+
if termination_mode == "process_group" and os.name != "nt":
|
|
122
|
+
if not isinstance(posix_pgid, int) or posix_pgid <= 1:
|
|
123
|
+
return None
|
|
124
|
+
pgid = posix_pgid
|
|
125
|
+
if cleanup_kind is not None:
|
|
126
|
+
if cleanup_kind != "docker-container" or not _valid_cleanup_id(cleanup_id):
|
|
127
|
+
return None
|
|
128
|
+
payload = {
|
|
129
|
+
"schema_version": _SCHEMA_VERSION,
|
|
130
|
+
"record_id": uuid.uuid4().hex,
|
|
131
|
+
"owner_pid": owner_pid,
|
|
132
|
+
"owner_start_token": owner_token,
|
|
133
|
+
"child_pid": child_pid,
|
|
134
|
+
"child_start_token": child_token,
|
|
135
|
+
"termination_mode": termination_mode,
|
|
136
|
+
"posix_pgid": pgid,
|
|
137
|
+
"cleanup_kind": cleanup_kind,
|
|
138
|
+
"cleanup_id": cleanup_id,
|
|
139
|
+
"created_at": time.time(),
|
|
140
|
+
}
|
|
141
|
+
try:
|
|
142
|
+
self.state_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
143
|
+
path = self.state_dir / f"{payload['record_id']}.json"
|
|
144
|
+
temporary = self.state_dir / f".{payload['record_id']}.{uuid.uuid4().hex}.tmp"
|
|
145
|
+
raw = (json.dumps(payload, sort_keys=True, separators=(",", ":")) + "\n").encode()
|
|
146
|
+
descriptor = os.open(temporary, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
147
|
+
try:
|
|
148
|
+
with os.fdopen(descriptor, "wb", closefd=True) as stream:
|
|
149
|
+
stream.write(raw)
|
|
150
|
+
stream.flush()
|
|
151
|
+
os.fsync(stream.fileno())
|
|
152
|
+
os.replace(temporary, path)
|
|
153
|
+
_fsync_directory(self.state_dir)
|
|
154
|
+
finally:
|
|
155
|
+
with contextlib.suppress(OSError):
|
|
156
|
+
temporary.unlink()
|
|
157
|
+
return TerminalOwnershipHandle(path)
|
|
158
|
+
except OSError:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def release(self, handle: TerminalOwnershipHandle | None) -> None:
|
|
162
|
+
if handle is None:
|
|
163
|
+
return
|
|
164
|
+
try:
|
|
165
|
+
resolved = handle.record_path.resolve()
|
|
166
|
+
if resolved.parent != self.state_dir or resolved.suffix != ".json":
|
|
167
|
+
return
|
|
168
|
+
except OSError:
|
|
169
|
+
return
|
|
170
|
+
_unlink_record(handle.record_path)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _read_record(path: Path) -> dict[str, Any] | None:
|
|
174
|
+
try:
|
|
175
|
+
info = path.lstat()
|
|
176
|
+
if not stat.S_ISREG(info.st_mode) or info.st_size <= 0 or info.st_size > _MAX_RECORD_BYTES:
|
|
177
|
+
return None
|
|
178
|
+
flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
|
|
179
|
+
descriptor = os.open(path, flags)
|
|
180
|
+
with os.fdopen(descriptor, "r", encoding="utf-8", closefd=True) as stream:
|
|
181
|
+
payload = json.load(stream)
|
|
182
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
183
|
+
return None
|
|
184
|
+
if not isinstance(payload, dict) or set(payload) != {
|
|
185
|
+
"schema_version",
|
|
186
|
+
"record_id",
|
|
187
|
+
"owner_pid",
|
|
188
|
+
"owner_start_token",
|
|
189
|
+
"child_pid",
|
|
190
|
+
"child_start_token",
|
|
191
|
+
"termination_mode",
|
|
192
|
+
"posix_pgid",
|
|
193
|
+
"cleanup_kind",
|
|
194
|
+
"cleanup_id",
|
|
195
|
+
"created_at",
|
|
196
|
+
}:
|
|
197
|
+
return None
|
|
198
|
+
if payload.get("schema_version") != _SCHEMA_VERSION:
|
|
199
|
+
return None
|
|
200
|
+
if (
|
|
201
|
+
not isinstance(payload.get("record_id"), str)
|
|
202
|
+
or re.fullmatch(r"[0-9a-f]{32}", payload["record_id"]) is None
|
|
203
|
+
):
|
|
204
|
+
return None
|
|
205
|
+
if path.name != f"{payload['record_id']}.json":
|
|
206
|
+
return None
|
|
207
|
+
for key in ("owner_pid", "child_pid"):
|
|
208
|
+
if (
|
|
209
|
+
not isinstance(payload.get(key), int)
|
|
210
|
+
or isinstance(payload[key], bool)
|
|
211
|
+
or payload[key] <= 1
|
|
212
|
+
):
|
|
213
|
+
return None
|
|
214
|
+
for key in ("owner_start_token", "child_start_token"):
|
|
215
|
+
if not isinstance(payload.get(key), str) or not payload[key] or len(payload[key]) > 256:
|
|
216
|
+
return None
|
|
217
|
+
if payload.get("termination_mode") not in {"process_group", "direct"}:
|
|
218
|
+
return None
|
|
219
|
+
pgid = payload.get("posix_pgid")
|
|
220
|
+
if pgid is not None and (not isinstance(pgid, int) or isinstance(pgid, bool) or pgid <= 1):
|
|
221
|
+
return None
|
|
222
|
+
cleanup_kind = payload.get("cleanup_kind")
|
|
223
|
+
cleanup_id = payload.get("cleanup_id")
|
|
224
|
+
if cleanup_kind is not None and (
|
|
225
|
+
cleanup_kind != "docker-container" or not _valid_cleanup_id(cleanup_id)
|
|
226
|
+
):
|
|
227
|
+
return None
|
|
228
|
+
if cleanup_kind is None and cleanup_id is not None:
|
|
229
|
+
return None
|
|
230
|
+
if not isinstance(payload.get("created_at"), (int, float)):
|
|
231
|
+
return None
|
|
232
|
+
return payload
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _valid_cleanup_id(value: Any) -> bool:
|
|
236
|
+
return isinstance(value, str) and _DOCKER_CONTAINER_PATTERN.fullmatch(value) is not None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _unlink_record(path: Path) -> None:
|
|
240
|
+
with contextlib.suppress(OSError):
|
|
241
|
+
path.unlink()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _process_state(pid: int, expected_token: str) -> _ProcessState:
|
|
245
|
+
existence = _pid_exists(pid)
|
|
246
|
+
if existence is False:
|
|
247
|
+
return "dead"
|
|
248
|
+
token = _pid_start_token(pid)
|
|
249
|
+
if token is not None:
|
|
250
|
+
return "matching" if token == expected_token else "reused"
|
|
251
|
+
return "unknown"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _pid_start_token(pid: int) -> str | None:
|
|
255
|
+
if not isinstance(pid, int) or isinstance(pid, bool) or pid <= 1:
|
|
256
|
+
return None
|
|
257
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows CI
|
|
258
|
+
return _windows_process_start_token(pid)
|
|
259
|
+
stat_path = Path("/proc") / str(pid) / "stat"
|
|
260
|
+
try:
|
|
261
|
+
text = stat_path.read_text(encoding="utf-8")
|
|
262
|
+
after_comm = text.rsplit(") ", 1)[1]
|
|
263
|
+
return f"linux:{after_comm.split()[19]}"
|
|
264
|
+
except (OSError, IndexError, ValueError):
|
|
265
|
+
pass
|
|
266
|
+
try:
|
|
267
|
+
completed = subprocess.run(
|
|
268
|
+
["ps", "-o", "lstart=", "-p", str(pid)],
|
|
269
|
+
check=False,
|
|
270
|
+
capture_output=True,
|
|
271
|
+
text=True,
|
|
272
|
+
timeout=2.0,
|
|
273
|
+
)
|
|
274
|
+
except (OSError, subprocess.SubprocessError):
|
|
275
|
+
return None
|
|
276
|
+
value = completed.stdout.strip()
|
|
277
|
+
return f"ps:{value}" if completed.returncode == 0 and value else None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _pid_exists(pid: int) -> bool | None:
|
|
281
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows CI
|
|
282
|
+
handle = _windows_open_process(pid, 0x00100000)
|
|
283
|
+
if not handle:
|
|
284
|
+
# OpenProcess uses ERROR_INVALID_PARAMETER for a PID that no longer exists. Access
|
|
285
|
+
# denied remains unknown and is deliberately preserved rather than guessed dead.
|
|
286
|
+
if ctypes.get_last_error() == 87:
|
|
287
|
+
return False
|
|
288
|
+
return None
|
|
289
|
+
try:
|
|
290
|
+
result = _windows_kernel32().WaitForSingleObject(handle, 0)
|
|
291
|
+
return result == 0x00000102
|
|
292
|
+
finally:
|
|
293
|
+
_windows_kernel32().CloseHandle(handle)
|
|
294
|
+
try:
|
|
295
|
+
os.kill(pid, 0)
|
|
296
|
+
except ProcessLookupError:
|
|
297
|
+
return False
|
|
298
|
+
except PermissionError:
|
|
299
|
+
return True
|
|
300
|
+
except OSError:
|
|
301
|
+
return None
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _windows_open_process(pid: int, access: int) -> Any:
|
|
306
|
+
try:
|
|
307
|
+
return _windows_kernel32().OpenProcess(access, False, pid)
|
|
308
|
+
except (AttributeError, OSError):
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _windows_kernel32() -> Any:
|
|
313
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
314
|
+
kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
|
|
315
|
+
kernel32.OpenProcess.restype = wintypes.HANDLE
|
|
316
|
+
kernel32.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
|
|
317
|
+
kernel32.WaitForSingleObject.restype = wintypes.DWORD
|
|
318
|
+
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
|
|
319
|
+
kernel32.CloseHandle.restype = wintypes.BOOL
|
|
320
|
+
kernel32.GetProcessTimes.argtypes = [
|
|
321
|
+
wintypes.HANDLE,
|
|
322
|
+
ctypes.POINTER(wintypes.FILETIME),
|
|
323
|
+
ctypes.POINTER(wintypes.FILETIME),
|
|
324
|
+
ctypes.POINTER(wintypes.FILETIME),
|
|
325
|
+
ctypes.POINTER(wintypes.FILETIME),
|
|
326
|
+
]
|
|
327
|
+
kernel32.GetProcessTimes.restype = wintypes.BOOL
|
|
328
|
+
return kernel32
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _windows_process_start_token(pid: int) -> str | None:
|
|
332
|
+
handle = _windows_open_process(pid, 0x1000) # PROCESS_QUERY_LIMITED_INFORMATION
|
|
333
|
+
if not handle:
|
|
334
|
+
return None
|
|
335
|
+
try:
|
|
336
|
+
created = wintypes.FILETIME()
|
|
337
|
+
exited = wintypes.FILETIME()
|
|
338
|
+
kernel = wintypes.FILETIME()
|
|
339
|
+
user = wintypes.FILETIME()
|
|
340
|
+
if not _windows_kernel32().GetProcessTimes(
|
|
341
|
+
handle,
|
|
342
|
+
ctypes.byref(created),
|
|
343
|
+
ctypes.byref(exited),
|
|
344
|
+
ctypes.byref(kernel),
|
|
345
|
+
ctypes.byref(user),
|
|
346
|
+
):
|
|
347
|
+
return None
|
|
348
|
+
ticks = (int(created.dwHighDateTime) << 32) | int(created.dwLowDateTime)
|
|
349
|
+
return f"windows:{ticks}"
|
|
350
|
+
except (AttributeError, OSError):
|
|
351
|
+
return None
|
|
352
|
+
finally:
|
|
353
|
+
_windows_kernel32().CloseHandle(handle)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _fsync_directory(path: Path) -> None:
|
|
357
|
+
if os.name == "nt":
|
|
358
|
+
return
|
|
359
|
+
try:
|
|
360
|
+
descriptor = os.open(path, os.O_RDONLY)
|
|
361
|
+
except OSError:
|
|
362
|
+
return
|
|
363
|
+
try:
|
|
364
|
+
os.fsync(descriptor)
|
|
365
|
+
except OSError:
|
|
366
|
+
pass
|
|
367
|
+
finally:
|
|
368
|
+
os.close(descriptor)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _terminate_owned_process(payload: dict[str, Any]) -> bool:
|
|
372
|
+
# Re-check immediately before signalling to narrow the PID-reuse race.
|
|
373
|
+
if _process_state(payload["child_pid"], payload["child_start_token"]) != "matching":
|
|
374
|
+
return False
|
|
375
|
+
_cleanup_owned_resource(payload)
|
|
376
|
+
child_pid = payload["child_pid"]
|
|
377
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows CI
|
|
378
|
+
return _terminate_windows_process_tree(child_pid, payload["child_start_token"])
|
|
379
|
+
if payload["termination_mode"] == "process_group":
|
|
380
|
+
pgid = payload.get("posix_pgid")
|
|
381
|
+
if pgid != child_pid or pgid == os.getpgrp():
|
|
382
|
+
return False
|
|
383
|
+
try:
|
|
384
|
+
if os.getpgid(child_pid) != pgid:
|
|
385
|
+
return False
|
|
386
|
+
except ProcessLookupError:
|
|
387
|
+
return True
|
|
388
|
+
return _terminate_posix_group(pgid)
|
|
389
|
+
try:
|
|
390
|
+
os.kill(child_pid, signal.SIGTERM)
|
|
391
|
+
except ProcessLookupError:
|
|
392
|
+
return True
|
|
393
|
+
except OSError:
|
|
394
|
+
return False
|
|
395
|
+
return True
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _terminate_windows_process_tree(child_pid: int, child_start_token: str) -> bool:
|
|
399
|
+
try:
|
|
400
|
+
subprocess.run(
|
|
401
|
+
["taskkill", "/PID", str(child_pid), "/T", "/F"],
|
|
402
|
+
check=False,
|
|
403
|
+
stdin=subprocess.DEVNULL,
|
|
404
|
+
stdout=subprocess.DEVNULL,
|
|
405
|
+
stderr=subprocess.DEVNULL,
|
|
406
|
+
timeout=5.0,
|
|
407
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
408
|
+
)
|
|
409
|
+
except (OSError, subprocess.SubprocessError):
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
# taskkill can return a non-zero status when the exact target exits during its own tree walk.
|
|
413
|
+
# Conversely, a zero status is not sufficient reason to discard durable ownership state until
|
|
414
|
+
# the recorded PID/token identity is confirmed gone. This also prevents a recycled PID from
|
|
415
|
+
# being mistaken for the process we attempted to terminate.
|
|
416
|
+
deadline = time.monotonic() + _TERMINATION_CONFIRM_TIMEOUT_S
|
|
417
|
+
while True:
|
|
418
|
+
state = _process_state(child_pid, child_start_token)
|
|
419
|
+
if state in {"dead", "reused"}:
|
|
420
|
+
return True
|
|
421
|
+
if time.monotonic() >= deadline:
|
|
422
|
+
return False
|
|
423
|
+
time.sleep(_TERMINATION_CONFIRM_INTERVAL_S)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _terminate_posix_group(pgid: int) -> bool:
|
|
427
|
+
try:
|
|
428
|
+
os.killpg(pgid, signal.SIGTERM)
|
|
429
|
+
except ProcessLookupError:
|
|
430
|
+
return True
|
|
431
|
+
except OSError:
|
|
432
|
+
return False
|
|
433
|
+
deadline = time.monotonic() + 1.0
|
|
434
|
+
while time.monotonic() < deadline:
|
|
435
|
+
try:
|
|
436
|
+
os.killpg(pgid, 0)
|
|
437
|
+
except ProcessLookupError:
|
|
438
|
+
return True
|
|
439
|
+
except OSError:
|
|
440
|
+
break
|
|
441
|
+
time.sleep(0.02)
|
|
442
|
+
with contextlib.suppress(ProcessLookupError):
|
|
443
|
+
os.killpg(pgid, signal.SIGKILL)
|
|
444
|
+
return True
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def _cleanup_owned_resource(payload: dict[str, Any]) -> None:
|
|
448
|
+
if payload.get("cleanup_kind") != "docker-container" or not _valid_cleanup_id(
|
|
449
|
+
payload.get("cleanup_id")
|
|
450
|
+
):
|
|
451
|
+
return
|
|
452
|
+
with contextlib.suppress(OSError, subprocess.SubprocessError):
|
|
453
|
+
subprocess.run(
|
|
454
|
+
["docker", "rm", "-f", payload["cleanup_id"]],
|
|
455
|
+
check=False,
|
|
456
|
+
stdin=subprocess.DEVNULL,
|
|
457
|
+
stdout=subprocess.DEVNULL,
|
|
458
|
+
stderr=subprocess.DEVNULL,
|
|
459
|
+
timeout=10.0,
|
|
460
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import unicodedata
|
|
4
|
+
|
|
5
|
+
_TYPOGRAPHIC_QUOTE_TRANSLATION = str.maketrans(
|
|
6
|
+
{
|
|
7
|
+
"\u2018": "'",
|
|
8
|
+
"\u2019": "'",
|
|
9
|
+
"\u201a": "'",
|
|
10
|
+
"\u201b": "'",
|
|
11
|
+
"\u201c": '"',
|
|
12
|
+
"\u201d": '"',
|
|
13
|
+
"\u201e": '"',
|
|
14
|
+
"\u201f": '"',
|
|
15
|
+
}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def normalize_for_matching(text: str) -> str:
|
|
20
|
+
"""Return a language-neutral Unicode/whitespace/quote normalization.
|
|
21
|
+
|
|
22
|
+
This helper is for comparing controller-owned markers and fingerprints. It
|
|
23
|
+
deliberately contains no vocabulary, stemming, translation, or
|
|
24
|
+
language-specific character rewriting and must not be used as an intent
|
|
25
|
+
classifier.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
normalized = unicodedata.normalize("NFKC", str(text or ""))
|
|
29
|
+
normalized = normalized.translate(_TYPOGRAPHIC_QUOTE_TRANSLATION).casefold()
|
|
30
|
+
return " ".join(normalized.split())
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import re
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
_TOKEN_ENCODING_NAME = "cl100k_base"
|
|
8
|
+
_TOKEN_LIKE_RE = re.compile(r"\w+|[^\w\s]", re.UNICODE)
|
|
9
|
+
_WORD_RE = re.compile(r"\w+", re.UNICODE)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@lru_cache(maxsize=1)
|
|
13
|
+
def _token_encoder() -> object | None:
|
|
14
|
+
try:
|
|
15
|
+
import tiktoken # type: ignore[import-not-found]
|
|
16
|
+
|
|
17
|
+
return tiktoken.get_encoding(_TOKEN_ENCODING_NAME)
|
|
18
|
+
except Exception:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _fallback_estimate_tokens(text: str) -> int:
|
|
23
|
+
pieces = _TOKEN_LIKE_RE.findall(text)
|
|
24
|
+
if not pieces:
|
|
25
|
+
return 1 if text else 0
|
|
26
|
+
|
|
27
|
+
total = 0
|
|
28
|
+
for piece in pieces:
|
|
29
|
+
if _WORD_RE.fullmatch(piece):
|
|
30
|
+
total += max(1, math.ceil(len(piece.encode("utf-8")) / 4))
|
|
31
|
+
else:
|
|
32
|
+
total += 1
|
|
33
|
+
return total
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def estimate_tokens(text: str) -> int:
|
|
37
|
+
if not text:
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
encoder = _token_encoder()
|
|
41
|
+
if encoder is not None:
|
|
42
|
+
try:
|
|
43
|
+
return len(encoder.encode(text)) # type: ignore[attr-defined]
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
return _fallback_estimate_tokens(text)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def trim_text_to_budget(text: str, max_tokens: int) -> tuple[str, bool]:
|
|
51
|
+
if max_tokens <= 0:
|
|
52
|
+
return "", bool(text)
|
|
53
|
+
|
|
54
|
+
current = estimate_tokens(text)
|
|
55
|
+
if current <= max_tokens:
|
|
56
|
+
return text, False
|
|
57
|
+
|
|
58
|
+
marker = "\n\n...[TRUNCATED FOR TOKEN BUDGET]...\n\n"
|
|
59
|
+
marker_tokens = max(1, estimate_tokens(marker))
|
|
60
|
+
|
|
61
|
+
if max_tokens <= marker_tokens + 2:
|
|
62
|
+
return marker, True
|
|
63
|
+
|
|
64
|
+
avg_chars_per_token = max(1, len(text) // max(1, current))
|
|
65
|
+
available = max_tokens - marker_tokens
|
|
66
|
+
head_tokens = available // 2
|
|
67
|
+
tail_tokens = available - head_tokens
|
|
68
|
+
head_chars = max(32, head_tokens * avg_chars_per_token)
|
|
69
|
+
tail_chars = max(32, tail_tokens * avg_chars_per_token)
|
|
70
|
+
|
|
71
|
+
def _build() -> str:
|
|
72
|
+
if head_chars + tail_chars >= len(text):
|
|
73
|
+
return text
|
|
74
|
+
return text[:head_chars] + marker + text[-tail_chars:]
|
|
75
|
+
|
|
76
|
+
candidate = _build()
|
|
77
|
+
while estimate_tokens(candidate) > max_tokens and (head_chars > 32 or tail_chars > 32):
|
|
78
|
+
head_chars = max(32, int(head_chars * 0.9))
|
|
79
|
+
tail_chars = max(32, int(tail_chars * 0.9))
|
|
80
|
+
candidate = _build()
|
|
81
|
+
|
|
82
|
+
return candidate, True
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def compute_input_budget(cap: object, safety_margin: int = 512) -> int:
|
|
86
|
+
context = int(getattr(cap, "context_window_tokens", 8192))
|
|
87
|
+
output = int(getattr(cap, "max_output_tokens", 2048))
|
|
88
|
+
if output >= context > 0:
|
|
89
|
+
# Shared-window metadata (e.g. the Kimi Code ids, where max_tokens may
|
|
90
|
+
# be raised up to the full context): there is no fixed output
|
|
91
|
+
# reservation, so subtracting the whole window would leave the 512-token
|
|
92
|
+
# floor as the input budget on a 1M-context model — the fresh-session
|
|
93
|
+
# "context: 0% left" footer bug. Reserve a conservative response
|
|
94
|
+
# allowance instead of the entire window.
|
|
95
|
+
output = min(output, max(4096, context // 8))
|
|
96
|
+
budget = context - output - max(0, int(safety_margin))
|
|
97
|
+
return max(512, budget)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Built-In Tools
|
|
2
|
+
|
|
3
|
+
This package contains Alysis Code's built-in tool implementations. These are
|
|
4
|
+
registered by the host at session startup after mode, workspace, sandbox, and
|
|
5
|
+
configuration checks have been applied.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- `fs.py`, `search.py`, `symbols.py`, `git.py`, and `history.py` cover local
|
|
10
|
+
workspace inspection and edits.
|
|
11
|
+
- `shell.py` delegates command execution to the configured shell runner.
|
|
12
|
+
- `web.py` and `web_search.py` provide bounded web access when configured.
|
|
13
|
+
- `registry.py` keeps the tool metadata, schemas, previews, and result summaries
|
|
14
|
+
used by the session runtime.
|
|
15
|
+
- `availability.py` tracks optional tools without making missing optional
|
|
16
|
+
backends a startup failure.
|
|
17
|
+
|
|
18
|
+
## Scope
|
|
19
|
+
|
|
20
|
+
Tool exposure is decided in `../agent/tools_assembly.py`; defining a tool here
|
|
21
|
+
does not make it available in every runtime or mode.
|
|
22
|
+
|
|
23
|
+
Shell execution policy is owned by the session and sandbox layers. Networked
|
|
24
|
+
tools should continue to use the safe HTTP path where applicable.
|
|
25
|
+
|
|
26
|
+
## Development
|
|
27
|
+
|
|
28
|
+
Keep changes narrow and run the tests for the tool family you touch. If a change
|
|
29
|
+
affects which tools are exposed, also cover the session assembly path.
|
|
30
|
+
|
|
31
|
+
## See Also
|
|
32
|
+
|
|
33
|
+
- [Security model](../../../docs/security_model.md)
|
|
34
|
+
- [Shell sandbox](../../../docs/shell_sandbox.md)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|