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,664 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
import uuid
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from contextlib import suppress
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from ..config import AgentRuntimeSettings
|
|
16
|
+
from .base import (
|
|
17
|
+
AuthMethod,
|
|
18
|
+
RuntimeAccountStatus,
|
|
19
|
+
RuntimeCapabilities,
|
|
20
|
+
RuntimeProbeStatus,
|
|
21
|
+
RuntimeTurnRequest,
|
|
22
|
+
RuntimeTurnResult,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_VERSION_TIMEOUT_SECONDS = 5.0
|
|
26
|
+
_STATUS_TIMEOUT_SECONDS = 10.0
|
|
27
|
+
_PROCESS_TREE_GRACE_SECONDS = 2.0
|
|
28
|
+
_TIMEOUT_EXIT_CODE = 124
|
|
29
|
+
_MISSING_EXECUTABLE_EXIT_CODE = 127
|
|
30
|
+
_INVALID_REQUEST_EXIT_CODE = 2
|
|
31
|
+
_FORCED_CHATGPT_LOGIN_CONFIG = 'forced_login_method="chatgpt"'
|
|
32
|
+
_RUNTIME_ENV_ALLOWLIST = frozenset(
|
|
33
|
+
{
|
|
34
|
+
"ALL_PROXY",
|
|
35
|
+
"BROWSER",
|
|
36
|
+
"CODEX_HOME",
|
|
37
|
+
"COLORTERM",
|
|
38
|
+
"COMSPEC",
|
|
39
|
+
"CURL_CA_BUNDLE",
|
|
40
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
41
|
+
"DISPLAY",
|
|
42
|
+
"HOME",
|
|
43
|
+
"HTTP_PROXY",
|
|
44
|
+
"HTTPS_PROXY",
|
|
45
|
+
"LANG",
|
|
46
|
+
"LC_ALL",
|
|
47
|
+
"LC_CTYPE",
|
|
48
|
+
"LOGNAME",
|
|
49
|
+
"NIX_SSL_CERT_FILE",
|
|
50
|
+
"NO_PROXY",
|
|
51
|
+
"PATH",
|
|
52
|
+
"PATHEXT",
|
|
53
|
+
"REQUESTS_CA_BUNDLE",
|
|
54
|
+
"SHELL",
|
|
55
|
+
"SSL_CERT_DIR",
|
|
56
|
+
"SSL_CERT_FILE",
|
|
57
|
+
"SYSTEMROOT",
|
|
58
|
+
"TEMP",
|
|
59
|
+
"TERM",
|
|
60
|
+
"TMP",
|
|
61
|
+
"TMPDIR",
|
|
62
|
+
"USER",
|
|
63
|
+
"USERPROFILE",
|
|
64
|
+
"WINDIR",
|
|
65
|
+
"XDG_CACHE_HOME",
|
|
66
|
+
"XDG_CONFIG_HOME",
|
|
67
|
+
"XDG_RUNTIME_DIR",
|
|
68
|
+
"WAYLAND_DISPLAY",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class CodexCliRuntimeAdapter:
|
|
74
|
+
"""Account and installation boundary for the official Codex CLI.
|
|
75
|
+
|
|
76
|
+
The adapter invokes documented Codex commands and intentionally never reads
|
|
77
|
+
Codex's credential files or transports an OAuth token through Alysis Code.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
runtime_id = "openai-codex"
|
|
81
|
+
adapter_id = "codex-cli"
|
|
82
|
+
display_name = "OpenAI Codex account"
|
|
83
|
+
description = "Delegate work to Codex using its official ChatGPT sign-in."
|
|
84
|
+
default_executable = "codex"
|
|
85
|
+
auth_hint = (
|
|
86
|
+
"Browser or device-code sign-in is managed by Codex; Alysis Code never reads its tokens."
|
|
87
|
+
)
|
|
88
|
+
capabilities = RuntimeCapabilities(
|
|
89
|
+
streaming=False,
|
|
90
|
+
session_resume=True,
|
|
91
|
+
image_inputs=True,
|
|
92
|
+
structured_output=True,
|
|
93
|
+
read_only=True,
|
|
94
|
+
workspace_write=True,
|
|
95
|
+
)
|
|
96
|
+
auth_methods = (
|
|
97
|
+
AuthMethod(id="browser", label="Sign in with ChatGPT in a browser"),
|
|
98
|
+
AuthMethod(id="device-code", label="Sign in with a device code"),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def probe(self, settings: AgentRuntimeSettings) -> RuntimeProbeStatus:
|
|
102
|
+
executable = _resolve_executable(settings.executable)
|
|
103
|
+
if executable is None:
|
|
104
|
+
return RuntimeProbeStatus(
|
|
105
|
+
available=False,
|
|
106
|
+
executable=settings.executable,
|
|
107
|
+
detail="Codex CLI is not installed or is not executable.",
|
|
108
|
+
)
|
|
109
|
+
try:
|
|
110
|
+
result = subprocess.run(
|
|
111
|
+
[executable, "--version"],
|
|
112
|
+
check=False,
|
|
113
|
+
capture_output=True,
|
|
114
|
+
text=True,
|
|
115
|
+
timeout=_VERSION_TIMEOUT_SECONDS,
|
|
116
|
+
env=_runtime_environment(),
|
|
117
|
+
)
|
|
118
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
119
|
+
return RuntimeProbeStatus(
|
|
120
|
+
available=False,
|
|
121
|
+
executable=executable,
|
|
122
|
+
detail=f"Codex version check failed: {exc}",
|
|
123
|
+
)
|
|
124
|
+
version = _combined_output(result)
|
|
125
|
+
if result.returncode != 0:
|
|
126
|
+
return RuntimeProbeStatus(
|
|
127
|
+
available=False,
|
|
128
|
+
executable=executable,
|
|
129
|
+
version=version or None,
|
|
130
|
+
detail="Codex version check returned a non-zero exit status.",
|
|
131
|
+
)
|
|
132
|
+
return RuntimeProbeStatus(
|
|
133
|
+
available=True,
|
|
134
|
+
executable=executable,
|
|
135
|
+
version=version or None,
|
|
136
|
+
detail="Codex CLI is available.",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def account_status(self, settings: AgentRuntimeSettings) -> RuntimeAccountStatus:
|
|
140
|
+
probe = self.probe(settings)
|
|
141
|
+
if not probe.available or not probe.executable:
|
|
142
|
+
return RuntimeAccountStatus(authenticated=False, verified=False, detail=probe.detail)
|
|
143
|
+
try:
|
|
144
|
+
result = subprocess.run(
|
|
145
|
+
[probe.executable, "login", "status"],
|
|
146
|
+
check=False,
|
|
147
|
+
capture_output=True,
|
|
148
|
+
text=True,
|
|
149
|
+
timeout=_STATUS_TIMEOUT_SECONDS,
|
|
150
|
+
env=_runtime_environment(),
|
|
151
|
+
)
|
|
152
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
153
|
+
return RuntimeAccountStatus(
|
|
154
|
+
authenticated=False,
|
|
155
|
+
verified=False,
|
|
156
|
+
detail=f"Codex login status check failed: {exc}",
|
|
157
|
+
)
|
|
158
|
+
output = _combined_output(result)
|
|
159
|
+
if result.returncode != 0:
|
|
160
|
+
return RuntimeAccountStatus(
|
|
161
|
+
authenticated=False,
|
|
162
|
+
detail=output or "Codex is not signed in.",
|
|
163
|
+
)
|
|
164
|
+
method = _login_method(output)
|
|
165
|
+
if method != "chatgpt":
|
|
166
|
+
return RuntimeAccountStatus(
|
|
167
|
+
authenticated=False,
|
|
168
|
+
verified=True,
|
|
169
|
+
auth_method_id=method,
|
|
170
|
+
detail=(
|
|
171
|
+
"Codex is signed in, but not with a ChatGPT account. "
|
|
172
|
+
"Run `alysis auth login openai-codex --switch-account`."
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
return RuntimeAccountStatus(
|
|
176
|
+
authenticated=True,
|
|
177
|
+
auth_method_id=method,
|
|
178
|
+
account_label=_account_label(output),
|
|
179
|
+
detail=output or "Codex is signed in.",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def login(
|
|
183
|
+
self,
|
|
184
|
+
settings: AgentRuntimeSettings,
|
|
185
|
+
method_id: str,
|
|
186
|
+
) -> RuntimeAccountStatus:
|
|
187
|
+
probe = self.probe(settings)
|
|
188
|
+
if not probe.available or not probe.executable:
|
|
189
|
+
return RuntimeAccountStatus(authenticated=False, verified=False, detail=probe.detail)
|
|
190
|
+
normalized = str(method_id or "").strip().lower()
|
|
191
|
+
if normalized not in {method.id for method in self.auth_methods}:
|
|
192
|
+
return RuntimeAccountStatus(
|
|
193
|
+
authenticated=False,
|
|
194
|
+
verified=False,
|
|
195
|
+
detail=f"Unsupported Codex authentication method: {method_id!r}.",
|
|
196
|
+
)
|
|
197
|
+
command = [probe.executable, "login"]
|
|
198
|
+
if normalized == "device-code":
|
|
199
|
+
command.append("--device-auth")
|
|
200
|
+
try:
|
|
201
|
+
result = subprocess.run(command, check=False, env=_runtime_environment())
|
|
202
|
+
except OSError as exc:
|
|
203
|
+
return RuntimeAccountStatus(
|
|
204
|
+
authenticated=False,
|
|
205
|
+
verified=False,
|
|
206
|
+
detail=f"Could not start Codex login: {exc}",
|
|
207
|
+
)
|
|
208
|
+
if result.returncode != 0:
|
|
209
|
+
return RuntimeAccountStatus(
|
|
210
|
+
authenticated=False,
|
|
211
|
+
verified=False,
|
|
212
|
+
detail="Codex login did not complete successfully.",
|
|
213
|
+
)
|
|
214
|
+
return self.account_status(settings)
|
|
215
|
+
|
|
216
|
+
def logout(self, settings: AgentRuntimeSettings) -> RuntimeAccountStatus:
|
|
217
|
+
probe = self.probe(settings)
|
|
218
|
+
if not probe.available or not probe.executable:
|
|
219
|
+
return RuntimeAccountStatus(authenticated=False, verified=False, detail=probe.detail)
|
|
220
|
+
try:
|
|
221
|
+
result = subprocess.run(
|
|
222
|
+
[probe.executable, "logout"],
|
|
223
|
+
check=False,
|
|
224
|
+
env=_runtime_environment(),
|
|
225
|
+
)
|
|
226
|
+
except OSError as exc:
|
|
227
|
+
return RuntimeAccountStatus(
|
|
228
|
+
authenticated=True,
|
|
229
|
+
verified=False,
|
|
230
|
+
detail=f"Could not start Codex logout: {exc}",
|
|
231
|
+
)
|
|
232
|
+
if result.returncode != 0:
|
|
233
|
+
return RuntimeAccountStatus(
|
|
234
|
+
authenticated=True,
|
|
235
|
+
verified=False,
|
|
236
|
+
detail="Codex logout did not complete successfully.",
|
|
237
|
+
)
|
|
238
|
+
return RuntimeAccountStatus(authenticated=False, detail="Codex is signed out.")
|
|
239
|
+
|
|
240
|
+
def run_turn(
|
|
241
|
+
self,
|
|
242
|
+
settings: AgentRuntimeSettings,
|
|
243
|
+
request: RuntimeTurnRequest,
|
|
244
|
+
) -> RuntimeTurnResult:
|
|
245
|
+
if not settings.provider_managed_auth:
|
|
246
|
+
raise ValueError("The Codex account runtime requires provider-managed authentication.")
|
|
247
|
+
sandbox = _codex_sandbox(request.mode)
|
|
248
|
+
cwd = Path(request.cwd).expanduser().resolve()
|
|
249
|
+
if not cwd.is_dir():
|
|
250
|
+
return RuntimeTurnResult(
|
|
251
|
+
runtime_id=self.runtime_id,
|
|
252
|
+
command=(),
|
|
253
|
+
exit_code=_INVALID_REQUEST_EXIT_CODE,
|
|
254
|
+
error=f"Delegated runtime cwd is not a directory: {cwd}",
|
|
255
|
+
)
|
|
256
|
+
# Validate resume identifiers before probing the host executable. Invalid
|
|
257
|
+
# option-shaped input is a request error even on hosts without Codex.
|
|
258
|
+
_validated_session_id(request.session_id)
|
|
259
|
+
executable = _resolve_executable(settings.executable)
|
|
260
|
+
if executable is None:
|
|
261
|
+
return RuntimeTurnResult(
|
|
262
|
+
runtime_id=self.runtime_id,
|
|
263
|
+
command=(),
|
|
264
|
+
exit_code=_MISSING_EXECUTABLE_EXIT_CODE,
|
|
265
|
+
error="Codex CLI is not installed or is not executable.",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
with tempfile.TemporaryDirectory(prefix="alysis-codex-turn-") as temp_dir:
|
|
269
|
+
final_message_path = Path(temp_dir) / "last-message.txt"
|
|
270
|
+
command = _build_turn_command(
|
|
271
|
+
executable=executable,
|
|
272
|
+
settings=settings,
|
|
273
|
+
request=request,
|
|
274
|
+
cwd=cwd,
|
|
275
|
+
sandbox=sandbox,
|
|
276
|
+
final_message_path=final_message_path,
|
|
277
|
+
)
|
|
278
|
+
stdout, stderr, exit_code, timed_out, launch_error = _run_process(
|
|
279
|
+
command,
|
|
280
|
+
prompt=request.prompt,
|
|
281
|
+
cwd=cwd,
|
|
282
|
+
timeout_seconds=settings.timeout_seconds,
|
|
283
|
+
)
|
|
284
|
+
events, warnings = _parse_jsonl_events(stdout)
|
|
285
|
+
if request.mode == "review":
|
|
286
|
+
warnings = (
|
|
287
|
+
"Review mode was enforced as read-only by the Codex runtime.",
|
|
288
|
+
*warnings,
|
|
289
|
+
)
|
|
290
|
+
final_message = _read_final_message(final_message_path) or _event_final_message(events)
|
|
291
|
+
session_id = (
|
|
292
|
+
None if request.no_log else (_event_session_id(events) or request.session_id)
|
|
293
|
+
)
|
|
294
|
+
usage = _event_usage(events)
|
|
295
|
+
error = launch_error
|
|
296
|
+
if timed_out:
|
|
297
|
+
error = f"Codex turn timed out after {settings.timeout_seconds:g} seconds."
|
|
298
|
+
elif exit_code != 0 and error is None:
|
|
299
|
+
error = (
|
|
300
|
+
_event_error(events)
|
|
301
|
+
or stderr.strip()
|
|
302
|
+
or (f"Codex turn exited with status {exit_code}.")
|
|
303
|
+
)
|
|
304
|
+
return RuntimeTurnResult(
|
|
305
|
+
runtime_id=self.runtime_id,
|
|
306
|
+
command=tuple(command),
|
|
307
|
+
exit_code=exit_code,
|
|
308
|
+
final_message=final_message,
|
|
309
|
+
session_id=session_id,
|
|
310
|
+
stdout=stdout,
|
|
311
|
+
stderr=stderr,
|
|
312
|
+
events=events,
|
|
313
|
+
usage=usage,
|
|
314
|
+
timed_out=timed_out,
|
|
315
|
+
error=error,
|
|
316
|
+
warnings=warnings,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _codex_sandbox(mode: str) -> str:
|
|
321
|
+
normalized = str(mode or "").strip().lower().replace("_", "-")
|
|
322
|
+
if normalized in {"readonly", "read-only", "review"}:
|
|
323
|
+
return "read-only"
|
|
324
|
+
if normalized in {"auto", "workspace-write"}:
|
|
325
|
+
return "workspace-write"
|
|
326
|
+
if normalized in {"fullaccess", "full-access", "danger-full-access"}:
|
|
327
|
+
raise ValueError(
|
|
328
|
+
"The Codex account runtime does not support fullaccess; use readonly, review, or auto."
|
|
329
|
+
)
|
|
330
|
+
raise ValueError(f"Unsupported delegated runtime mode: {mode!r}.")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _build_turn_command(
|
|
334
|
+
*,
|
|
335
|
+
executable: str,
|
|
336
|
+
settings: AgentRuntimeSettings,
|
|
337
|
+
request: RuntimeTurnRequest,
|
|
338
|
+
cwd: Path,
|
|
339
|
+
sandbox: str,
|
|
340
|
+
final_message_path: Path,
|
|
341
|
+
) -> list[str]:
|
|
342
|
+
session_id = _validated_session_id(request.session_id)
|
|
343
|
+
if session_id:
|
|
344
|
+
command = [
|
|
345
|
+
executable,
|
|
346
|
+
"exec",
|
|
347
|
+
"resume",
|
|
348
|
+
"--json",
|
|
349
|
+
"-c",
|
|
350
|
+
_FORCED_CHATGPT_LOGIN_CONFIG,
|
|
351
|
+
"-c",
|
|
352
|
+
f'sandbox_mode="{sandbox}"',
|
|
353
|
+
"--ignore-user-config",
|
|
354
|
+
"--ignore-rules",
|
|
355
|
+
]
|
|
356
|
+
else:
|
|
357
|
+
command = [
|
|
358
|
+
executable,
|
|
359
|
+
"exec",
|
|
360
|
+
"--json",
|
|
361
|
+
"--sandbox",
|
|
362
|
+
sandbox,
|
|
363
|
+
"-C",
|
|
364
|
+
os.fspath(cwd),
|
|
365
|
+
"-c",
|
|
366
|
+
_FORCED_CHATGPT_LOGIN_CONFIG,
|
|
367
|
+
"--ignore-user-config",
|
|
368
|
+
"--ignore-rules",
|
|
369
|
+
]
|
|
370
|
+
command.extend(
|
|
371
|
+
[
|
|
372
|
+
"--skip-git-repo-check",
|
|
373
|
+
"--output-last-message",
|
|
374
|
+
os.fspath(final_message_path),
|
|
375
|
+
]
|
|
376
|
+
)
|
|
377
|
+
model = str(settings.model or "").strip()
|
|
378
|
+
if model:
|
|
379
|
+
command.extend(["--model", model])
|
|
380
|
+
for image in request.images:
|
|
381
|
+
image_path = Path(image).expanduser()
|
|
382
|
+
if not image_path.is_absolute():
|
|
383
|
+
image_path = cwd / image_path
|
|
384
|
+
command.extend(["--image", os.fspath(image_path.resolve())])
|
|
385
|
+
if request.no_log:
|
|
386
|
+
command.append("--ephemeral")
|
|
387
|
+
if session_id:
|
|
388
|
+
command.append("--")
|
|
389
|
+
command.append(session_id)
|
|
390
|
+
command.append("-")
|
|
391
|
+
return command
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _run_process(
|
|
395
|
+
command: list[str],
|
|
396
|
+
*,
|
|
397
|
+
prompt: str,
|
|
398
|
+
cwd: Path,
|
|
399
|
+
timeout_seconds: float,
|
|
400
|
+
) -> tuple[str, str, int, bool, str | None]:
|
|
401
|
+
popen_kwargs: dict[str, Any] = {
|
|
402
|
+
"cwd": os.fspath(cwd),
|
|
403
|
+
"env": _runtime_environment(),
|
|
404
|
+
"stdin": subprocess.PIPE,
|
|
405
|
+
"stdout": subprocess.PIPE,
|
|
406
|
+
"stderr": subprocess.PIPE,
|
|
407
|
+
"text": True,
|
|
408
|
+
"encoding": "utf-8",
|
|
409
|
+
"errors": "replace",
|
|
410
|
+
}
|
|
411
|
+
if os.name == "nt":
|
|
412
|
+
popen_kwargs["creationflags"] = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
|
|
413
|
+
else:
|
|
414
|
+
popen_kwargs["start_new_session"] = True
|
|
415
|
+
try:
|
|
416
|
+
process = subprocess.Popen(command, **popen_kwargs)
|
|
417
|
+
except OSError as exc:
|
|
418
|
+
message = f"Could not start Codex CLI: {exc}"
|
|
419
|
+
return "", message, _MISSING_EXECUTABLE_EXIT_CODE, False, message
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
stdout, stderr = process.communicate(input=str(prompt), timeout=float(timeout_seconds))
|
|
423
|
+
except subprocess.TimeoutExpired as exc:
|
|
424
|
+
partial_stdout = _coerce_process_text(exc.stdout)
|
|
425
|
+
partial_stderr = _coerce_process_text(exc.stderr)
|
|
426
|
+
_terminate_process_tree(process)
|
|
427
|
+
try:
|
|
428
|
+
stdout, stderr = process.communicate()
|
|
429
|
+
except (OSError, ValueError):
|
|
430
|
+
stdout, stderr = partial_stdout, partial_stderr
|
|
431
|
+
return (
|
|
432
|
+
_coerce_process_text(stdout) or partial_stdout,
|
|
433
|
+
_coerce_process_text(stderr) or partial_stderr,
|
|
434
|
+
_TIMEOUT_EXIT_CODE,
|
|
435
|
+
True,
|
|
436
|
+
None,
|
|
437
|
+
)
|
|
438
|
+
except BaseException:
|
|
439
|
+
# KeyboardInterrupt/SystemExit must not orphan a provider process (or
|
|
440
|
+
# any descendants) after Alysis Code returns control to the terminal.
|
|
441
|
+
with suppress(BaseException):
|
|
442
|
+
_terminate_process_tree(process)
|
|
443
|
+
raise
|
|
444
|
+
return (
|
|
445
|
+
_coerce_process_text(stdout),
|
|
446
|
+
_coerce_process_text(stderr),
|
|
447
|
+
int(process.returncode if process.returncode is not None else 1),
|
|
448
|
+
False,
|
|
449
|
+
None,
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _runtime_environment(source: Mapping[str, str] | None = None) -> dict[str, str]:
|
|
454
|
+
environment = source if source is not None else os.environ
|
|
455
|
+
allowed: dict[str, str] = {}
|
|
456
|
+
for key, value in environment.items():
|
|
457
|
+
normalized = str(key).upper()
|
|
458
|
+
if normalized not in _RUNTIME_ENV_ALLOWLIST:
|
|
459
|
+
continue
|
|
460
|
+
if normalized.endswith(("_API_KEY", "_TOKEN", "_SECRET", "_PASSWORD")):
|
|
461
|
+
continue
|
|
462
|
+
allowed[str(key)] = str(value)
|
|
463
|
+
return allowed
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _terminate_process_tree(process: subprocess.Popen[str]) -> None:
|
|
467
|
+
if process.poll() is not None:
|
|
468
|
+
return
|
|
469
|
+
if os.name == "nt":
|
|
470
|
+
with suppress(OSError, subprocess.TimeoutExpired):
|
|
471
|
+
subprocess.run(
|
|
472
|
+
["taskkill", "/PID", str(process.pid), "/T", "/F"],
|
|
473
|
+
check=False,
|
|
474
|
+
stdout=subprocess.DEVNULL,
|
|
475
|
+
stderr=subprocess.DEVNULL,
|
|
476
|
+
timeout=_PROCESS_TREE_GRACE_SECONDS,
|
|
477
|
+
)
|
|
478
|
+
if process.poll() is None:
|
|
479
|
+
with suppress(OSError):
|
|
480
|
+
process.kill()
|
|
481
|
+
with suppress(OSError, subprocess.TimeoutExpired):
|
|
482
|
+
process.wait(timeout=_PROCESS_TREE_GRACE_SECONDS)
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
used_process_group = False
|
|
486
|
+
try:
|
|
487
|
+
os.killpg(process.pid, signal.SIGTERM)
|
|
488
|
+
used_process_group = True
|
|
489
|
+
except ProcessLookupError:
|
|
490
|
+
return
|
|
491
|
+
except OSError:
|
|
492
|
+
with suppress(OSError):
|
|
493
|
+
process.terminate()
|
|
494
|
+
with suppress(OSError, subprocess.TimeoutExpired):
|
|
495
|
+
process.wait(timeout=_PROCESS_TREE_GRACE_SECONDS)
|
|
496
|
+
if used_process_group:
|
|
497
|
+
# The parent may have exited while a child ignored SIGTERM. Because the
|
|
498
|
+
# process started in a dedicated session, killing the group is safe and
|
|
499
|
+
# guarantees that no descendant survives the timed-out turn.
|
|
500
|
+
with suppress(ProcessLookupError, OSError):
|
|
501
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
502
|
+
elif process.poll() is None:
|
|
503
|
+
with suppress(OSError):
|
|
504
|
+
process.kill()
|
|
505
|
+
with suppress(OSError, subprocess.TimeoutExpired):
|
|
506
|
+
process.wait(timeout=_PROCESS_TREE_GRACE_SECONDS)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _coerce_process_text(value: object) -> str:
|
|
510
|
+
if value is None:
|
|
511
|
+
return ""
|
|
512
|
+
if isinstance(value, bytes):
|
|
513
|
+
return value.decode("utf-8", errors="replace")
|
|
514
|
+
return str(value)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _parse_jsonl_events(
|
|
518
|
+
stdout: str,
|
|
519
|
+
) -> tuple[tuple[Mapping[str, object], ...], tuple[str, ...]]:
|
|
520
|
+
events: list[Mapping[str, object]] = []
|
|
521
|
+
warnings: list[str] = []
|
|
522
|
+
for line_number, line in enumerate(str(stdout or "").splitlines(), start=1):
|
|
523
|
+
text = line.strip()
|
|
524
|
+
if not text:
|
|
525
|
+
continue
|
|
526
|
+
try:
|
|
527
|
+
parsed = json.loads(text)
|
|
528
|
+
except json.JSONDecodeError:
|
|
529
|
+
warnings.append(f"Ignored invalid Codex JSONL event on line {line_number}.")
|
|
530
|
+
continue
|
|
531
|
+
if not isinstance(parsed, dict):
|
|
532
|
+
warnings.append(f"Ignored non-object Codex JSONL event on line {line_number}.")
|
|
533
|
+
continue
|
|
534
|
+
events.append(parsed)
|
|
535
|
+
return tuple(events), tuple(warnings)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _read_final_message(path: Path) -> str:
|
|
539
|
+
try:
|
|
540
|
+
return path.read_text(encoding="utf-8").strip()
|
|
541
|
+
except OSError:
|
|
542
|
+
return ""
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _event_final_message(events: tuple[Mapping[str, object], ...]) -> str:
|
|
546
|
+
for event in reversed(events):
|
|
547
|
+
item = event.get("item")
|
|
548
|
+
if isinstance(item, Mapping) and str(item.get("type") or "") == "agent_message":
|
|
549
|
+
text = _message_text(item)
|
|
550
|
+
if text:
|
|
551
|
+
return text
|
|
552
|
+
event_type = str(event.get("type") or "")
|
|
553
|
+
if event_type in {"agent_message", "assistant_message", "message.completed"}:
|
|
554
|
+
text = _message_text(event)
|
|
555
|
+
if text:
|
|
556
|
+
return text
|
|
557
|
+
return ""
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _message_text(value: Mapping[str, object]) -> str:
|
|
561
|
+
direct = value.get("text") or value.get("message")
|
|
562
|
+
if isinstance(direct, str):
|
|
563
|
+
return direct.strip()
|
|
564
|
+
content = value.get("content")
|
|
565
|
+
if isinstance(content, str):
|
|
566
|
+
return content.strip()
|
|
567
|
+
if isinstance(content, list):
|
|
568
|
+
parts: list[str] = []
|
|
569
|
+
for block in content:
|
|
570
|
+
if isinstance(block, Mapping):
|
|
571
|
+
text = block.get("text")
|
|
572
|
+
if isinstance(text, str) and text.strip():
|
|
573
|
+
parts.append(text.strip())
|
|
574
|
+
return "\n".join(parts)
|
|
575
|
+
return ""
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _event_session_id(events: tuple[Mapping[str, object], ...]) -> str | None:
|
|
579
|
+
for event in events:
|
|
580
|
+
for key in ("thread_id", "session_id"):
|
|
581
|
+
value = event.get(key)
|
|
582
|
+
if isinstance(value, str) and value.strip():
|
|
583
|
+
return value.strip()
|
|
584
|
+
thread = event.get("thread")
|
|
585
|
+
if isinstance(thread, Mapping):
|
|
586
|
+
value = thread.get("id")
|
|
587
|
+
if isinstance(value, str) and value.strip():
|
|
588
|
+
return value.strip()
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def _event_usage(events: tuple[Mapping[str, object], ...]) -> Mapping[str, object] | None:
|
|
593
|
+
for event in reversed(events):
|
|
594
|
+
usage = event.get("usage")
|
|
595
|
+
if isinstance(usage, Mapping):
|
|
596
|
+
return dict(usage)
|
|
597
|
+
return None
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def _event_error(events: tuple[Mapping[str, object], ...]) -> str | None:
|
|
601
|
+
for event in reversed(events):
|
|
602
|
+
event_type = str(event.get("type") or "")
|
|
603
|
+
if event_type not in {"error", "turn.failed", "item.failed"}:
|
|
604
|
+
continue
|
|
605
|
+
error = event.get("error")
|
|
606
|
+
if isinstance(error, Mapping):
|
|
607
|
+
message = error.get("message")
|
|
608
|
+
if isinstance(message, str) and message.strip():
|
|
609
|
+
return message.strip()
|
|
610
|
+
message = event.get("message")
|
|
611
|
+
if isinstance(message, str) and message.strip():
|
|
612
|
+
return message.strip()
|
|
613
|
+
return None
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _resolve_executable(configured: str) -> str | None:
|
|
617
|
+
value = str(configured or "").strip()
|
|
618
|
+
if not value:
|
|
619
|
+
return None
|
|
620
|
+
if os.path.basename(value) == value:
|
|
621
|
+
return shutil.which(value)
|
|
622
|
+
path = Path(value).expanduser()
|
|
623
|
+
if not path.is_file() or not os.access(path, os.X_OK):
|
|
624
|
+
return None
|
|
625
|
+
return os.fspath(path)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _validated_session_id(raw: str | None) -> str:
|
|
629
|
+
value = str(raw or "").strip()
|
|
630
|
+
if not value:
|
|
631
|
+
return ""
|
|
632
|
+
try:
|
|
633
|
+
return str(uuid.UUID(value))
|
|
634
|
+
except ValueError as exc:
|
|
635
|
+
raise ValueError("Delegated Codex session ids must be UUIDs.") from exc
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _combined_output(result: subprocess.CompletedProcess[str]) -> str:
|
|
639
|
+
lines = []
|
|
640
|
+
for line in str(result.stdout or result.stderr or "").splitlines():
|
|
641
|
+
if line.strip().casefold().startswith("warning: proceeding"):
|
|
642
|
+
continue
|
|
643
|
+
lines.append(line)
|
|
644
|
+
return "\n".join(lines).strip()
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _login_method(output: str) -> str | None:
|
|
648
|
+
normalized = str(output or "").casefold()
|
|
649
|
+
if "chatgpt" in normalized or "chat gpt" in normalized:
|
|
650
|
+
return "chatgpt"
|
|
651
|
+
if "api key" in normalized or "api-key" in normalized:
|
|
652
|
+
return "api-key"
|
|
653
|
+
return "provider-managed"
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _account_label(output: str) -> str | None:
|
|
657
|
+
for line in str(output or "").splitlines():
|
|
658
|
+
stripped = line.strip()
|
|
659
|
+
if "@" in stripped and len(stripped) <= 200:
|
|
660
|
+
return stripped
|
|
661
|
+
return None
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
__all__ = ["CodexCliRuntimeAdapter"]
|