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,367 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import sys
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from .auth import require_token
|
|
10
|
+
from .job_config import resolve_effective_model_base_url
|
|
11
|
+
from .settings import ServerSettings
|
|
12
|
+
from .store import ServerStore, ServerStoreError
|
|
13
|
+
from .worker_runner import JobRunner
|
|
14
|
+
|
|
15
|
+
_UPLOAD_CHUNK_BYTES = 1024 * 1024
|
|
16
|
+
_AgentMode = Literal["auto", "review", "readonly", "fullaccess"]
|
|
17
|
+
_SwarmMode = Literal["auto"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RunJobRequest(BaseModel):
|
|
21
|
+
instruction: str
|
|
22
|
+
mode: _AgentMode = "auto"
|
|
23
|
+
yes: bool = True
|
|
24
|
+
model: str | None = None
|
|
25
|
+
base_url: str | None = None
|
|
26
|
+
temperature: float | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ForgeExecRequest(BaseModel):
|
|
30
|
+
task_id: str = Field(min_length=1)
|
|
31
|
+
mode: _AgentMode = "auto"
|
|
32
|
+
yes: bool = True
|
|
33
|
+
model: str | None = None
|
|
34
|
+
base_url: str | None = None
|
|
35
|
+
temperature: float | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ForgeSwarmRequest(BaseModel):
|
|
39
|
+
# Non-dry-run swarm orchestration is auto-only by runtime invariant.
|
|
40
|
+
mode: _SwarmMode = "auto"
|
|
41
|
+
yes: bool = True
|
|
42
|
+
model: str | None = None
|
|
43
|
+
base_url: str | None = None
|
|
44
|
+
temperature: float | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def _close_upload_best_effort(upload: object) -> None:
|
|
48
|
+
close = getattr(upload, "close", None)
|
|
49
|
+
if not callable(close):
|
|
50
|
+
return
|
|
51
|
+
try:
|
|
52
|
+
result = close()
|
|
53
|
+
if inspect.isawaitable(result):
|
|
54
|
+
await result
|
|
55
|
+
except Exception:
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def _stage_uploaded_zip(
|
|
60
|
+
*,
|
|
61
|
+
upload: object,
|
|
62
|
+
max_upload_bytes: int,
|
|
63
|
+
chunk_size: int = _UPLOAD_CHUNK_BYTES,
|
|
64
|
+
) -> Path:
|
|
65
|
+
if chunk_size <= 0:
|
|
66
|
+
raise ValueError("chunk_size must be > 0")
|
|
67
|
+
|
|
68
|
+
temp_path: Path | None = None
|
|
69
|
+
total_bytes = 0
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as handle:
|
|
73
|
+
temp_path = Path(handle.name)
|
|
74
|
+
while True:
|
|
75
|
+
chunk = await upload.read(chunk_size) # type: ignore[attr-defined]
|
|
76
|
+
if not chunk:
|
|
77
|
+
break
|
|
78
|
+
next_total = total_bytes + len(chunk)
|
|
79
|
+
if next_total > max_upload_bytes:
|
|
80
|
+
raise ServerStoreError(
|
|
81
|
+
f"Upload too large ({next_total} bytes); max is {max_upload_bytes} bytes."
|
|
82
|
+
)
|
|
83
|
+
handle.write(chunk)
|
|
84
|
+
total_bytes = next_total
|
|
85
|
+
return temp_path
|
|
86
|
+
except Exception:
|
|
87
|
+
if temp_path is not None:
|
|
88
|
+
try:
|
|
89
|
+
temp_path.unlink()
|
|
90
|
+
except OSError:
|
|
91
|
+
pass
|
|
92
|
+
raise
|
|
93
|
+
finally:
|
|
94
|
+
await _close_upload_best_effort(upload)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
async def _create_run_from_upload(
|
|
98
|
+
*,
|
|
99
|
+
store: ServerStore,
|
|
100
|
+
upload: object,
|
|
101
|
+
max_upload_bytes: int,
|
|
102
|
+
chunk_size: int = _UPLOAD_CHUNK_BYTES,
|
|
103
|
+
) -> str:
|
|
104
|
+
# Stage uploads to disk so size enforcement happens during request reads, not after
|
|
105
|
+
# buffering the full multipart body into memory.
|
|
106
|
+
staged_path = await _stage_uploaded_zip(
|
|
107
|
+
upload=upload,
|
|
108
|
+
max_upload_bytes=max_upload_bytes,
|
|
109
|
+
chunk_size=chunk_size,
|
|
110
|
+
)
|
|
111
|
+
try:
|
|
112
|
+
return store.create_run_from_zip_path(staged_path)
|
|
113
|
+
finally:
|
|
114
|
+
try:
|
|
115
|
+
staged_path.unlink()
|
|
116
|
+
except OSError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _append_common_agent_args(
|
|
121
|
+
command: list[str],
|
|
122
|
+
*,
|
|
123
|
+
mode: str,
|
|
124
|
+
yes: bool,
|
|
125
|
+
model: str,
|
|
126
|
+
base_url: str | None,
|
|
127
|
+
temperature: float | None,
|
|
128
|
+
) -> None:
|
|
129
|
+
command.extend(["--mode", mode, "--api-key-env", "ALYSIS_API_KEY"])
|
|
130
|
+
if yes:
|
|
131
|
+
command.append("--yes")
|
|
132
|
+
command.extend(["--model", model])
|
|
133
|
+
if base_url:
|
|
134
|
+
command.extend(["--base-url", base_url])
|
|
135
|
+
if temperature is not None:
|
|
136
|
+
command.extend(["--temperature", str(temperature)])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _agent_entrypoint_prefix(settings: ServerSettings) -> list[str]:
|
|
140
|
+
if settings.worker_backend == "docker":
|
|
141
|
+
return ["python", "-m", "alysis_code"]
|
|
142
|
+
return [sys.executable, "-m", "alysis_code"]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _append_forge_machine_flag(command: list[str], settings: ServerSettings) -> None:
|
|
146
|
+
"""Ask Forge workers for the NDJSON event stream so job status is reported, not guessed."""
|
|
147
|
+
if settings.worker_machine_events:
|
|
148
|
+
command.append("--machine")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _validate_forge_exec_task_id(task_id: str) -> None:
|
|
152
|
+
if task_id.startswith("-"):
|
|
153
|
+
raise ValueError("Invalid task_id: must not start with '-'")
|
|
154
|
+
if any(ch.isspace() for ch in task_id):
|
|
155
|
+
raise ValueError("Invalid task_id: whitespace is not allowed")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _build_run_job_command(
|
|
159
|
+
*,
|
|
160
|
+
settings: ServerSettings,
|
|
161
|
+
req: RunJobRequest,
|
|
162
|
+
model: str,
|
|
163
|
+
base_url: str | None,
|
|
164
|
+
) -> list[str]:
|
|
165
|
+
command = _agent_entrypoint_prefix(settings) + ["run", "--path", "/workspace"]
|
|
166
|
+
_append_common_agent_args(
|
|
167
|
+
command,
|
|
168
|
+
mode=req.mode,
|
|
169
|
+
yes=req.yes,
|
|
170
|
+
model=model,
|
|
171
|
+
base_url=base_url,
|
|
172
|
+
temperature=req.temperature,
|
|
173
|
+
)
|
|
174
|
+
# End-of-options sentinel to prevent instruction option injection.
|
|
175
|
+
command.extend(["--", req.instruction])
|
|
176
|
+
return command
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _build_forge_exec_job_command(
|
|
180
|
+
*,
|
|
181
|
+
settings: ServerSettings,
|
|
182
|
+
req: ForgeExecRequest,
|
|
183
|
+
model: str,
|
|
184
|
+
base_url: str | None,
|
|
185
|
+
) -> list[str]:
|
|
186
|
+
_validate_forge_exec_task_id(req.task_id)
|
|
187
|
+
command = _agent_entrypoint_prefix(settings) + [
|
|
188
|
+
"forge",
|
|
189
|
+
"exec",
|
|
190
|
+
req.task_id,
|
|
191
|
+
"--path",
|
|
192
|
+
"/workspace",
|
|
193
|
+
]
|
|
194
|
+
_append_forge_machine_flag(command, settings)
|
|
195
|
+
_append_common_agent_args(
|
|
196
|
+
command,
|
|
197
|
+
mode=req.mode,
|
|
198
|
+
yes=req.yes,
|
|
199
|
+
model=model,
|
|
200
|
+
base_url=base_url,
|
|
201
|
+
temperature=req.temperature,
|
|
202
|
+
)
|
|
203
|
+
return command
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _build_forge_swarm_job_command(
|
|
207
|
+
*,
|
|
208
|
+
settings: ServerSettings,
|
|
209
|
+
req: ForgeSwarmRequest,
|
|
210
|
+
model: str,
|
|
211
|
+
base_url: str | None,
|
|
212
|
+
) -> list[str]:
|
|
213
|
+
command = _agent_entrypoint_prefix(settings) + [
|
|
214
|
+
"forge",
|
|
215
|
+
"swarm",
|
|
216
|
+
"--path",
|
|
217
|
+
"/workspace",
|
|
218
|
+
]
|
|
219
|
+
_append_forge_machine_flag(command, settings)
|
|
220
|
+
_append_common_agent_args(
|
|
221
|
+
command,
|
|
222
|
+
mode=req.mode,
|
|
223
|
+
yes=req.yes,
|
|
224
|
+
model=model,
|
|
225
|
+
base_url=base_url,
|
|
226
|
+
temperature=req.temperature,
|
|
227
|
+
)
|
|
228
|
+
return command
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def create_app(settings: ServerSettings): # type: ignore[no-untyped-def]
|
|
232
|
+
from fastapi import Depends, FastAPI, File, HTTPException, UploadFile, status
|
|
233
|
+
from fastapi.responses import PlainTextResponse
|
|
234
|
+
|
|
235
|
+
app = FastAPI(title="alysis-code server", version="1.0")
|
|
236
|
+
store = ServerStore(settings)
|
|
237
|
+
runner = JobRunner(settings, store)
|
|
238
|
+
auth_dep = require_token(settings)
|
|
239
|
+
|
|
240
|
+
@app.get("/health")
|
|
241
|
+
def health() -> dict[str, bool]:
|
|
242
|
+
return {"ok": True}
|
|
243
|
+
|
|
244
|
+
@app.post("/v1/runs", dependencies=[Depends(auth_dep)])
|
|
245
|
+
async def create_run(file: UploadFile = File(...)) -> dict[str, str]:
|
|
246
|
+
try:
|
|
247
|
+
run_id = await _create_run_from_upload(
|
|
248
|
+
store=store,
|
|
249
|
+
upload=file,
|
|
250
|
+
max_upload_bytes=settings.max_upload_bytes,
|
|
251
|
+
)
|
|
252
|
+
except ServerStoreError as e:
|
|
253
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
|
|
254
|
+
return {"run_id": run_id}
|
|
255
|
+
|
|
256
|
+
@app.post("/v1/runs/empty", dependencies=[Depends(auth_dep)])
|
|
257
|
+
def create_empty_run() -> dict[str, str]:
|
|
258
|
+
run_id = store.create_empty_run()
|
|
259
|
+
return {"run_id": run_id}
|
|
260
|
+
|
|
261
|
+
@app.post("/v1/runs/{run_id}/jobs/run", dependencies=[Depends(auth_dep)])
|
|
262
|
+
def start_run_job(run_id: str, req: RunJobRequest) -> dict[str, str]:
|
|
263
|
+
try:
|
|
264
|
+
effective_model, effective_base_url = resolve_effective_model_base_url(
|
|
265
|
+
settings=settings,
|
|
266
|
+
requested_model=req.model,
|
|
267
|
+
requested_base_url=req.base_url,
|
|
268
|
+
)
|
|
269
|
+
command = _build_run_job_command(
|
|
270
|
+
settings=settings,
|
|
271
|
+
req=req,
|
|
272
|
+
model=effective_model,
|
|
273
|
+
base_url=effective_base_url,
|
|
274
|
+
)
|
|
275
|
+
except ValueError as e:
|
|
276
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
job_id = runner.start_job(run_id=run_id, command=command)
|
|
280
|
+
except ServerStoreError as e:
|
|
281
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
|
|
282
|
+
return {"job_id": job_id}
|
|
283
|
+
|
|
284
|
+
@app.post("/v1/runs/{run_id}/jobs/forge_exec", dependencies=[Depends(auth_dep)])
|
|
285
|
+
def start_forge_exec_job(run_id: str, req: ForgeExecRequest) -> dict[str, str]:
|
|
286
|
+
try:
|
|
287
|
+
effective_model, effective_base_url = resolve_effective_model_base_url(
|
|
288
|
+
settings=settings,
|
|
289
|
+
requested_model=req.model,
|
|
290
|
+
requested_base_url=req.base_url,
|
|
291
|
+
)
|
|
292
|
+
command = _build_forge_exec_job_command(
|
|
293
|
+
settings=settings,
|
|
294
|
+
req=req,
|
|
295
|
+
model=effective_model,
|
|
296
|
+
base_url=effective_base_url,
|
|
297
|
+
)
|
|
298
|
+
except ValueError as e:
|
|
299
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
job_id = runner.start_job(run_id=run_id, command=command)
|
|
303
|
+
except ServerStoreError as e:
|
|
304
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
|
|
305
|
+
return {"job_id": job_id}
|
|
306
|
+
|
|
307
|
+
@app.post("/v1/runs/{run_id}/jobs/forge_swarm", dependencies=[Depends(auth_dep)])
|
|
308
|
+
def start_forge_swarm_job(run_id: str, req: ForgeSwarmRequest) -> dict[str, str]:
|
|
309
|
+
try:
|
|
310
|
+
effective_model, effective_base_url = resolve_effective_model_base_url(
|
|
311
|
+
settings=settings,
|
|
312
|
+
requested_model=req.model,
|
|
313
|
+
requested_base_url=req.base_url,
|
|
314
|
+
)
|
|
315
|
+
command = _build_forge_swarm_job_command(
|
|
316
|
+
settings=settings,
|
|
317
|
+
req=req,
|
|
318
|
+
model=effective_model,
|
|
319
|
+
base_url=effective_base_url,
|
|
320
|
+
)
|
|
321
|
+
except ValueError as e:
|
|
322
|
+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
|
|
323
|
+
|
|
324
|
+
try:
|
|
325
|
+
job_id = runner.start_job(run_id=run_id, command=command)
|
|
326
|
+
except ServerStoreError as e:
|
|
327
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
|
|
328
|
+
return {"job_id": job_id}
|
|
329
|
+
|
|
330
|
+
@app.get("/v1/jobs/{job_id}", dependencies=[Depends(auth_dep)])
|
|
331
|
+
def get_job_status(job_id: str) -> dict[str, object]:
|
|
332
|
+
try:
|
|
333
|
+
status_obj = runner.get_status(job_id)
|
|
334
|
+
except ServerStoreError as e:
|
|
335
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
|
|
336
|
+
return {
|
|
337
|
+
"job_id": status_obj.job_id,
|
|
338
|
+
"run_id": status_obj.run_id,
|
|
339
|
+
"status": status_obj.status,
|
|
340
|
+
# How the status was decided: the worker's terminal event, or the exit code.
|
|
341
|
+
"status_source": status_obj.status_source,
|
|
342
|
+
"created_at": status_obj.created_at,
|
|
343
|
+
"started_at": status_obj.started_at,
|
|
344
|
+
"finished_at": status_obj.finished_at,
|
|
345
|
+
"exit_code": status_obj.exit_code,
|
|
346
|
+
"error": status_obj.error,
|
|
347
|
+
"terminal_event": status_obj.terminal_event,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@app.get("/v1/jobs/{job_id}/logs", dependencies=[Depends(auth_dep)])
|
|
351
|
+
def get_job_logs(job_id: str) -> PlainTextResponse:
|
|
352
|
+
try:
|
|
353
|
+
body = runner.read_logs(job_id)
|
|
354
|
+
except ServerStoreError as e:
|
|
355
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
|
|
356
|
+
return PlainTextResponse(body)
|
|
357
|
+
|
|
358
|
+
@app.post("/v1/jobs/{job_id}/cancel", dependencies=[Depends(auth_dep)])
|
|
359
|
+
def cancel_job(job_id: str) -> dict[str, str]:
|
|
360
|
+
try:
|
|
361
|
+
runner.cancel_job(job_id)
|
|
362
|
+
status_obj = runner.get_status(job_id)
|
|
363
|
+
except ServerStoreError as e:
|
|
364
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e)) from e
|
|
365
|
+
return {"job_id": status_obj.job_id, "status": status_obj.status}
|
|
366
|
+
|
|
367
|
+
return app
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
|
|
3
|
+
from .settings import ServerSettings
|
|
4
|
+
|
|
5
|
+
_LOCAL_CLIENTS = {"127.0.0.1", "::1", "localhost"}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def require_token(settings: ServerSettings) -> Callable[..., None]:
|
|
9
|
+
from fastapi import HTTPException, Request, status
|
|
10
|
+
|
|
11
|
+
async def _dependency(request: Request) -> None:
|
|
12
|
+
client_host = (request.client.host if request.client else "") or ""
|
|
13
|
+
if settings.token:
|
|
14
|
+
auth_value = request.headers.get("authorization") or ""
|
|
15
|
+
if not auth_value.lower().startswith("bearer "):
|
|
16
|
+
raise HTTPException(
|
|
17
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
18
|
+
detail="Missing Bearer token.",
|
|
19
|
+
)
|
|
20
|
+
token = auth_value[7:].strip()
|
|
21
|
+
if token != settings.token:
|
|
22
|
+
raise HTTPException(
|
|
23
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
24
|
+
detail="Invalid token.",
|
|
25
|
+
)
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
if client_host not in _LOCAL_CLIENTS:
|
|
29
|
+
raise HTTPException(
|
|
30
|
+
status_code=status.HTTP_403_FORBIDDEN,
|
|
31
|
+
detail="Server token is not set; only localhost clients are allowed.",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return _dependency
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .settings import ServerSettings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def resolve_effective_model_base_url(
|
|
7
|
+
settings: ServerSettings,
|
|
8
|
+
requested_model: str | None,
|
|
9
|
+
requested_base_url: str | None,
|
|
10
|
+
) -> tuple[str, str | None]:
|
|
11
|
+
req_model = (requested_model or "").strip() or None
|
|
12
|
+
req_base_url = (requested_base_url or "").strip() or None
|
|
13
|
+
|
|
14
|
+
if settings.default_model:
|
|
15
|
+
model = settings.default_model
|
|
16
|
+
elif settings.allow_client_model and req_model:
|
|
17
|
+
model = req_model
|
|
18
|
+
else:
|
|
19
|
+
raise ValueError("Model is required (set ALYSIS_SERVER_MODEL or provide request.model).")
|
|
20
|
+
|
|
21
|
+
if settings.default_base_url:
|
|
22
|
+
base_url = settings.default_base_url
|
|
23
|
+
elif settings.allow_client_base_url and req_base_url:
|
|
24
|
+
if not req_base_url.startswith(("http://", "https://")):
|
|
25
|
+
raise ValueError("Client base_url must use http:// or https://.")
|
|
26
|
+
base_url = req_base_url
|
|
27
|
+
else:
|
|
28
|
+
base_url = None
|
|
29
|
+
|
|
30
|
+
return model, base_url
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..branding import canonical_server_data_dir, env_get
|
|
8
|
+
from ..config import ConfigError
|
|
9
|
+
|
|
10
|
+
_LOCAL_HOSTS = {"127.0.0.1", "::1", "localhost"}
|
|
11
|
+
_WORKER_BACKENDS = {"bwrap", "docker"}
|
|
12
|
+
_SANDBOX_MODES = {"off", "warn", "strict"}
|
|
13
|
+
_NETWORK_MODES = {"off", "on"}
|
|
14
|
+
_DEFAULT_MAX_UPLOAD_BYTES = 50 * 1024 * 1024
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _parse_int(raw: object, *, field_name: str, default: int, min_value: int) -> int:
|
|
18
|
+
if raw is None:
|
|
19
|
+
return default
|
|
20
|
+
try:
|
|
21
|
+
value = int(str(raw).strip())
|
|
22
|
+
except ValueError as e:
|
|
23
|
+
raise ConfigError(f"Invalid {field_name}: {raw!r}. Expected integer.") from e
|
|
24
|
+
if value < min_value:
|
|
25
|
+
raise ConfigError(f"Invalid {field_name}: {raw!r}. Expected >= {min_value}.")
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_choice(raw: object, *, field_name: str, default: str, allowed: set[str]) -> str:
|
|
30
|
+
if raw is None:
|
|
31
|
+
return default
|
|
32
|
+
value = str(raw).strip().lower()
|
|
33
|
+
if value in allowed:
|
|
34
|
+
return value
|
|
35
|
+
options = ", ".join(sorted(allowed))
|
|
36
|
+
raise ConfigError(f"Invalid {field_name}: {raw!r}. Expected one of: {options}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_bool(raw: object, *, field_name: str, default: bool) -> bool:
|
|
40
|
+
if raw is None:
|
|
41
|
+
return default
|
|
42
|
+
if isinstance(raw, bool):
|
|
43
|
+
return raw
|
|
44
|
+
value = str(raw).strip().lower()
|
|
45
|
+
if value in {"1", "true", "yes", "on"}:
|
|
46
|
+
return True
|
|
47
|
+
if value in {"0", "false", "no", "off"}:
|
|
48
|
+
return False
|
|
49
|
+
raise ConfigError(f"Invalid {field_name}: {raw!r}. Expected one of: 0, 1")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _parse_optional_string(raw: object) -> str | None:
|
|
53
|
+
if raw is None:
|
|
54
|
+
return None
|
|
55
|
+
value = str(raw).strip()
|
|
56
|
+
return value or None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _parse_optional_base_url(raw: object, *, field_name: str) -> str | None:
|
|
60
|
+
value = _parse_optional_string(raw)
|
|
61
|
+
if value is None:
|
|
62
|
+
return None
|
|
63
|
+
if value.startswith(("http://", "https://")):
|
|
64
|
+
return value
|
|
65
|
+
raise ConfigError(f"Invalid {field_name}: {value!r}. Expected http:// or https:// URL.")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _default_data_dir() -> Path:
|
|
69
|
+
return canonical_server_data_dir()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _default_worker_backend() -> str:
|
|
73
|
+
if platform.system().lower() == "linux":
|
|
74
|
+
return "bwrap"
|
|
75
|
+
return "docker"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class ServerSettings:
|
|
80
|
+
host: str
|
|
81
|
+
port: int
|
|
82
|
+
data_dir: Path
|
|
83
|
+
token: str | None
|
|
84
|
+
max_upload_bytes: int
|
|
85
|
+
max_concurrent_jobs: int
|
|
86
|
+
worker_backend: str
|
|
87
|
+
worker_sandbox_mode: str
|
|
88
|
+
worker_network: str
|
|
89
|
+
default_model: str | None
|
|
90
|
+
default_base_url: str | None
|
|
91
|
+
allow_client_model: bool
|
|
92
|
+
allow_client_base_url: bool
|
|
93
|
+
worker_machine_events: bool = True
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def allow_unauthenticated_localhost_only(self) -> bool:
|
|
97
|
+
return self.token is None
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def host_is_local(self) -> bool:
|
|
101
|
+
return self.host.strip().lower() in _LOCAL_HOSTS
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def server_model(self) -> str | None:
|
|
105
|
+
return self.default_model
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def server_base_url(self) -> str | None:
|
|
109
|
+
return self.default_base_url
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def allow_client_model_override(self) -> bool:
|
|
113
|
+
return self.allow_client_model
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def allow_client_base_url_override(self) -> bool:
|
|
117
|
+
return self.allow_client_base_url
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def resolve_server_settings(
|
|
121
|
+
*,
|
|
122
|
+
host: str = "127.0.0.1",
|
|
123
|
+
port: int = 7070,
|
|
124
|
+
data_dir: Path | None = None,
|
|
125
|
+
) -> ServerSettings:
|
|
126
|
+
host_value = host.strip() or "127.0.0.1"
|
|
127
|
+
port_value = _parse_int(port, field_name="port", default=7070, min_value=1)
|
|
128
|
+
if port_value > 65535:
|
|
129
|
+
raise ConfigError(f"Invalid port: {port_value}. Expected <= 65535.")
|
|
130
|
+
|
|
131
|
+
env_data_dir = str(env_get("ALYSIS_SERVER_DATA_DIR") or "").strip()
|
|
132
|
+
if data_dir is not None:
|
|
133
|
+
data_path = data_dir.expanduser()
|
|
134
|
+
elif env_data_dir:
|
|
135
|
+
data_path = Path(env_data_dir).expanduser()
|
|
136
|
+
else:
|
|
137
|
+
data_path = _default_data_dir()
|
|
138
|
+
|
|
139
|
+
token_raw = str(env_get("ALYSIS_SERVER_TOKEN") or "").strip()
|
|
140
|
+
token = token_raw or None
|
|
141
|
+
|
|
142
|
+
max_upload_bytes = _parse_int(
|
|
143
|
+
env_get("ALYSIS_SERVER_MAX_UPLOAD_BYTES"),
|
|
144
|
+
field_name="ALYSIS_SERVER_MAX_UPLOAD_BYTES",
|
|
145
|
+
default=_DEFAULT_MAX_UPLOAD_BYTES,
|
|
146
|
+
min_value=1024,
|
|
147
|
+
)
|
|
148
|
+
max_concurrent_jobs = _parse_int(
|
|
149
|
+
env_get("ALYSIS_SERVER_MAX_JOBS"),
|
|
150
|
+
field_name="ALYSIS_SERVER_MAX_JOBS",
|
|
151
|
+
default=2,
|
|
152
|
+
min_value=1,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
worker_backend = _parse_choice(
|
|
156
|
+
env_get("ALYSIS_SERVER_WORKER_BACKEND"),
|
|
157
|
+
field_name="ALYSIS_SERVER_WORKER_BACKEND",
|
|
158
|
+
default=_default_worker_backend(),
|
|
159
|
+
allowed=_WORKER_BACKENDS,
|
|
160
|
+
)
|
|
161
|
+
if worker_backend == "bwrap" and platform.system().lower() != "linux":
|
|
162
|
+
raise ConfigError("ALYSIS_SERVER_WORKER_BACKEND=bwrap requires Linux. Use docker instead.")
|
|
163
|
+
|
|
164
|
+
worker_sandbox_default = "strict"
|
|
165
|
+
worker_sandbox_mode = _parse_choice(
|
|
166
|
+
env_get("ALYSIS_SERVER_WORKER_SANDBOX_MODE"),
|
|
167
|
+
field_name="ALYSIS_SERVER_WORKER_SANDBOX_MODE",
|
|
168
|
+
default=worker_sandbox_default,
|
|
169
|
+
allowed=_SANDBOX_MODES,
|
|
170
|
+
)
|
|
171
|
+
worker_network = _parse_choice(
|
|
172
|
+
env_get("ALYSIS_SERVER_WORKER_NETWORK"),
|
|
173
|
+
field_name="ALYSIS_SERVER_WORKER_NETWORK",
|
|
174
|
+
default="on",
|
|
175
|
+
allowed=_NETWORK_MODES,
|
|
176
|
+
)
|
|
177
|
+
default_model = _parse_optional_string(env_get("ALYSIS_SERVER_MODEL"))
|
|
178
|
+
default_base_url = _parse_optional_base_url(
|
|
179
|
+
env_get("ALYSIS_SERVER_BASE_URL"),
|
|
180
|
+
field_name="ALYSIS_SERVER_BASE_URL",
|
|
181
|
+
)
|
|
182
|
+
allow_client_base_url = _parse_bool(
|
|
183
|
+
env_get("ALYSIS_SERVER_ALLOW_CLIENT_BASE_URL"),
|
|
184
|
+
field_name="ALYSIS_SERVER_ALLOW_CLIENT_BASE_URL",
|
|
185
|
+
default=False,
|
|
186
|
+
)
|
|
187
|
+
allow_client_model = _parse_bool(
|
|
188
|
+
env_get("ALYSIS_SERVER_ALLOW_CLIENT_MODEL"),
|
|
189
|
+
field_name="ALYSIS_SERVER_ALLOW_CLIENT_MODEL",
|
|
190
|
+
default=True,
|
|
191
|
+
)
|
|
192
|
+
# Forge worker jobs run with --machine so job status comes from the worker's own
|
|
193
|
+
# terminal event instead of being inferred from an exit code.
|
|
194
|
+
worker_machine_events = _parse_bool(
|
|
195
|
+
env_get("ALYSIS_SERVER_MACHINE_EVENTS"),
|
|
196
|
+
field_name="ALYSIS_SERVER_MACHINE_EVENTS",
|
|
197
|
+
default=True,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return ServerSettings(
|
|
201
|
+
host=host_value,
|
|
202
|
+
port=port_value,
|
|
203
|
+
data_dir=data_path.resolve(),
|
|
204
|
+
token=token,
|
|
205
|
+
max_upload_bytes=max_upload_bytes,
|
|
206
|
+
max_concurrent_jobs=max_concurrent_jobs,
|
|
207
|
+
worker_backend=worker_backend,
|
|
208
|
+
worker_sandbox_mode=worker_sandbox_mode,
|
|
209
|
+
worker_network=worker_network,
|
|
210
|
+
default_model=default_model,
|
|
211
|
+
default_base_url=default_base_url,
|
|
212
|
+
allow_client_model=allow_client_model,
|
|
213
|
+
allow_client_base_url=allow_client_base_url,
|
|
214
|
+
worker_machine_events=worker_machine_events,
|
|
215
|
+
)
|