devops-bot-sdk 1.6.38__tar.gz → 1.6.39__tar.gz
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.
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/__init__.py +1 -1
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ipc/handlers.py +24 -3
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/platform_compat.py +31 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/run_auto.py +33 -13
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/updater.py +227 -67
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_firewall_open_on_update.py +40 -6
- devops_bot_sdk-1.6.39/tests/test_run_verdict_is_honest.py +147 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_task_file_and_plan_run.py +6 -2
- devops_bot_sdk-1.6.39/tests/test_updater_reports_the_truth.py +328 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/README.md +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/client.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/firewall.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_vps_health.py +0 -0
- {devops_bot_sdk-1.6.38 → devops_bot_sdk-1.6.39}/tests/test_write_hook.py +0 -0
|
@@ -169,6 +169,7 @@ tests/test_resume.py
|
|
|
169
169
|
tests/test_router.py
|
|
170
170
|
tests/test_run_auto_agent_only.py
|
|
171
171
|
tests/test_run_outputs.py
|
|
172
|
+
tests/test_run_verdict_is_honest.py
|
|
172
173
|
tests/test_schedule_api.py
|
|
173
174
|
tests/test_scheduler.py
|
|
174
175
|
tests/test_self_report.py
|
|
@@ -184,6 +185,7 @@ tests/test_ui_gate.py
|
|
|
184
185
|
tests/test_ui_lifespan.py
|
|
185
186
|
tests/test_update_timer.py
|
|
186
187
|
tests/test_updater_extras.py
|
|
188
|
+
tests/test_updater_reports_the_truth.py
|
|
187
189
|
tests/test_url_access.py
|
|
188
190
|
tests/test_vps_health.py
|
|
189
191
|
tests/test_write_hook.py
|
|
@@ -34,7 +34,7 @@ Rules:
|
|
|
34
34
|
- All data egress through submit_webhook only
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
__version__ = "1.6.
|
|
37
|
+
__version__ = "1.6.39" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
|
|
38
38
|
__author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
|
|
39
39
|
|
|
40
40
|
from sdk.client import BackendClient
|
|
@@ -2083,10 +2083,20 @@ async def _run_local_pipeline(
|
|
|
2083
2083
|
await asyncio.sleep(_KICKOFF_BACKOFF_S * kick_attempt)
|
|
2084
2084
|
|
|
2085
2085
|
if not session_id:
|
|
2086
|
+
# Name the call and the window. "ReadTimeout" on its own reads like a
|
|
2087
|
+
# local fault and gets diagnosed as one (a proxy, a permissions problem,
|
|
2088
|
+
# the wrong user account); what it actually means here is that the backend
|
|
2089
|
+
# accepted the connection and then did not ack the kickoff in time.
|
|
2090
|
+
detail = last_error
|
|
2091
|
+
if "Timeout" in last_error:
|
|
2092
|
+
detail = (f"{last_error} — POST /api/v1/orchestrate connected but sent "
|
|
2093
|
+
f"no session_id within {_kick_read_s:.0f}s. The backend is "
|
|
2094
|
+
f"slow or wedged, not this machine; raise the window with "
|
|
2095
|
+
f"AGENTOS_ORCHESTRATE_KICKOFF_TIMEOUT_S if it is merely slow.")
|
|
2086
2096
|
await send(_error_envelope(
|
|
2087
2097
|
thread_id,
|
|
2088
2098
|
RuntimeError(
|
|
2089
|
-
f"orchestrate failed after {_KICKOFF_ATTEMPTS} attempt(s): {
|
|
2099
|
+
f"orchestrate failed after {_KICKOFF_ATTEMPTS} attempt(s): {detail}"
|
|
2090
2100
|
),
|
|
2091
2101
|
).model_dump())
|
|
2092
2102
|
return
|
|
@@ -5212,10 +5222,21 @@ async def handle_orchestrate_auto_local_run(
|
|
|
5212
5222
|
).model_dump())
|
|
5213
5223
|
continue
|
|
5214
5224
|
|
|
5225
|
+
# The run's status is what it ACHIEVED, not the fact that the loop reached
|
|
5226
|
+
# its end. Every per-task failure above is handled (logged, ticket moved to
|
|
5227
|
+
# Backlog, `continue`) so the loop always arrives here — and reporting that
|
|
5228
|
+
# as "completed" told an operator whose only ticket had just died on a
|
|
5229
|
+
# kickoff timeout that the run had succeeded, with `processed: 0` the sole
|
|
5230
|
+
# contradicting evidence. `processed` counts the tasks that were actually
|
|
5231
|
+
# carried through, so it is the honest basis for the verdict.
|
|
5232
|
+
status = ("completed" if processed == total
|
|
5233
|
+
else "failed" if processed == 0
|
|
5234
|
+
else "partial")
|
|
5215
5235
|
await send(Envelope(
|
|
5216
5236
|
type=Envelope.TYPE_DONE, thread_id=thread_id,
|
|
5217
|
-
data={"mode": "orchestrate-auto-local", "status":
|
|
5218
|
-
"processed": processed, "task_count": total
|
|
5237
|
+
data={"mode": "orchestrate-auto-local", "status": status,
|
|
5238
|
+
"processed": processed, "task_count": total,
|
|
5239
|
+
"failed": total - processed},
|
|
5219
5240
|
).model_dump())
|
|
5220
5241
|
|
|
5221
5242
|
except Exception as exc:
|
|
@@ -9,6 +9,37 @@ from __future__ import annotations
|
|
|
9
9
|
import os
|
|
10
10
|
import sys
|
|
11
11
|
|
|
12
|
+
#: Non-cp1252 glyphs the SDK prints → an ASCII stand-in. See :func:`safe_print`.
|
|
13
|
+
_ASCII_FALLBACK = str.maketrans({
|
|
14
|
+
"✓": "OK", "✗": "x", "→": "->", "↻": "~", "⚠": "!", "…": "...",
|
|
15
|
+
"▶": ">", "•": "-", "⏸": "||", "·": "-", "◐": "~",
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def safe_print(message: str = "") -> None:
|
|
20
|
+
"""``print`` that cannot take a command down on a legacy Windows codepage.
|
|
21
|
+
|
|
22
|
+
:func:`ensure_utf8_stdio` is the real fix and every entry point installs it.
|
|
23
|
+
This is the backstop for the case it cannot reach: a stdout with no
|
|
24
|
+
``reconfigure`` (pythonw, or a stream already swapped for a plain file
|
|
25
|
+
object), where the first ``✓``/``▶`` still raises ``UnicodeEncodeError``.
|
|
26
|
+
|
|
27
|
+
Worth the belt and braces because the failure mode is not a garbled line but
|
|
28
|
+
a dead run — and it lands in the two places that hurt most. In the updater,
|
|
29
|
+
the banner prints BEFORE the upgrade that would fix it, so the crash recurs
|
|
30
|
+
on every run and the release that repairs it can never install itself. In
|
|
31
|
+
``run_auto._send``, the glyph raises inside the ERROR handler, so the real
|
|
32
|
+
error is replaced by a UnicodeEncodeError traceback and is never seen at all.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
print(message)
|
|
36
|
+
except UnicodeEncodeError:
|
|
37
|
+
try:
|
|
38
|
+
print(message.translate(_ASCII_FALLBACK))
|
|
39
|
+
except UnicodeEncodeError:
|
|
40
|
+
# A glyph outside the table (engine output echoed into the line).
|
|
41
|
+
print(message.encode("ascii", "replace").decode("ascii"))
|
|
42
|
+
|
|
12
43
|
|
|
13
44
|
def ensure_utf8_stdio() -> None:
|
|
14
45
|
"""Make stdout/stderr able to carry the SDK's ``✓ ↻ → ⚠`` glyphs ANYWHERE.
|
|
@@ -67,6 +67,10 @@ import os
|
|
|
67
67
|
import sys
|
|
68
68
|
from pathlib import Path
|
|
69
69
|
|
|
70
|
+
# Import-cheap and dependency-free. safe_print degrades ✓/▶/✗ to ASCII when the
|
|
71
|
+
# stream cannot hold them — the backstop behind ensure_utf8_stdio; see _send.
|
|
72
|
+
from sdk.platform_compat import safe_print as _print
|
|
73
|
+
|
|
70
74
|
_AGENTOS_DIR = Path.home() / ".agentos"
|
|
71
75
|
_STOP_FILE = _AGENTOS_DIR / "auto.stop"
|
|
72
76
|
_PID_FILE = _AGENTOS_DIR / "auto.pid"
|
|
@@ -540,8 +544,8 @@ def _run_report(argv: list[str]) -> None:
|
|
|
540
544
|
mirrored into ``~/.agentos/auto.log`` so it is visible in
|
|
541
545
|
``agentos-auto logs -f``, which is where the summary line lands anyway.
|
|
542
546
|
"""
|
|
543
|
-
from sdk.exceptions import TokenNotConfigured
|
|
544
547
|
from sdk import self_report
|
|
548
|
+
from sdk.exceptions import TokenNotConfigured
|
|
545
549
|
|
|
546
550
|
texts = _flag_values(argv, "--text")
|
|
547
551
|
files = _flag_values(argv, "--file")
|
|
@@ -911,6 +915,14 @@ _current_task: str | None = None
|
|
|
911
915
|
|
|
912
916
|
|
|
913
917
|
async def _send(env: dict) -> None:
|
|
918
|
+
"""Render one pipeline envelope to the console.
|
|
919
|
+
|
|
920
|
+
Prints through :func:`~sdk.platform_compat.safe_print`, not ``print``. Every
|
|
921
|
+
line here carries a glyph cp1252 cannot hold, and this function is where a
|
|
922
|
+
Windows encoding crash did the most damage: the ``✗`` below runs INSIDE the
|
|
923
|
+
handler's error path, so a stdout the shim could not reconfigure turned every
|
|
924
|
+
real failure into a UnicodeEncodeError traceback with the actual error lost.
|
|
925
|
+
"""
|
|
914
926
|
etype = env.get("type", "")
|
|
915
927
|
data = env.get("data", {}) or {}
|
|
916
928
|
|
|
@@ -930,22 +942,22 @@ async def _send(env: dict) -> None:
|
|
|
930
942
|
# A task-file run has nothing to do with the ticket queue; calling it
|
|
931
943
|
# "orchestrate-auto" in the log sends the reader looking for a board.
|
|
932
944
|
repos = ", ".join(Path(r).name for r in (data.get("repos") or []))
|
|
933
|
-
|
|
945
|
+
_print(f"\n▶ plan (no ticket) — {data.get('title') or 'task file'}"
|
|
934
946
|
+ (f" · {repos}" if repos else "") + "\n")
|
|
935
947
|
else:
|
|
936
|
-
|
|
948
|
+
_print(f"\n▶ orchestrate-auto (local) — {data.get('task_count', 0)} task(s)\n")
|
|
937
949
|
elif etype == "step_update":
|
|
938
950
|
step = data.get("current_step", "")
|
|
939
951
|
pp = data.get("project_path")
|
|
940
|
-
|
|
952
|
+
_print(f" • {step}" + (f" [{pp}]" if pp else "") + _agent_suffix(data))
|
|
941
953
|
elif etype == "awaiting_approval":
|
|
942
|
-
|
|
954
|
+
_print(f" ⏸ approval gate: {data.get('summary', data.get('jira_key', ''))}")
|
|
943
955
|
elif etype == "error":
|
|
944
|
-
|
|
956
|
+
_print(f" ✗ {data.get('message') or data.get('code') or data}")
|
|
945
957
|
elif etype == "done" and data.get("mode") == "plan-local":
|
|
946
958
|
status = data.get("status", "")
|
|
947
959
|
mark = {"completed": "✓", "partial": "◐"}.get(status, "✗")
|
|
948
|
-
|
|
960
|
+
_print(f"\n{mark} plan {status} — {data.get('files_changed', 0)} file(s) changed")
|
|
949
961
|
for repo in data.get("repos") or []:
|
|
950
962
|
line = f" {'✓' if repo.get('succeeded') else '✗'} {Path(repo.get('repo') or '').name}"
|
|
951
963
|
if repo.get("branch"):
|
|
@@ -954,19 +966,27 @@ async def _send(env: dict) -> None:
|
|
|
954
966
|
line += f" · {repo['pr']}"
|
|
955
967
|
if repo.get("note"):
|
|
956
968
|
line += f" · {repo['note']}"
|
|
957
|
-
|
|
969
|
+
_print(line)
|
|
958
970
|
for issue in repo.get("issues") or []:
|
|
959
|
-
|
|
960
|
-
|
|
971
|
+
_print(f" ! {issue}")
|
|
972
|
+
_print()
|
|
961
973
|
elif etype == "done":
|
|
962
974
|
status = data.get("status", "")
|
|
975
|
+
done_n, total_n = data.get("processed", 0), data.get("task_count", 0)
|
|
963
976
|
if status == "paused":
|
|
964
|
-
|
|
977
|
+
_print(f"\n⏸ paused after {done_n}/{total_n} "
|
|
965
978
|
f"task(s). run `agentos-auto resume` to continue.\n")
|
|
979
|
+
elif status in ("completed", "partial", "failed"):
|
|
980
|
+
# A ✓ on a run that carried nothing through is the same lie the
|
|
981
|
+
# handler used to tell (see its status line): mark what happened.
|
|
982
|
+
mark = {"completed": "✓", "partial": "◐"}.get(status, "✗")
|
|
983
|
+
count = f"{done_n}/{total_n}" if total_n else str(done_n)
|
|
984
|
+
_print(f"\n{mark} {status} — {count} task(s) carried through"
|
|
985
|
+
+ (" (see the failures above)" if done_n < total_n else "") + "\n")
|
|
966
986
|
else:
|
|
967
|
-
|
|
987
|
+
_print(f"\n✓ done — {json.dumps(data)[:400]}\n")
|
|
968
988
|
else:
|
|
969
|
-
|
|
989
|
+
_print(f" [{etype}] {json.dumps(data)[:200]}")
|
|
970
990
|
|
|
971
991
|
|
|
972
992
|
def _run_loop(workspace_root: str | None, only_keys: list[str] | None = None,
|
|
@@ -18,6 +18,13 @@ import os
|
|
|
18
18
|
import platform
|
|
19
19
|
import subprocess
|
|
20
20
|
import sys
|
|
21
|
+
from typing import NamedTuple
|
|
22
|
+
|
|
23
|
+
# Import-cheap and dependency-free (it runs before anything else at startup).
|
|
24
|
+
# _ASCII_FALLBACK is re-exported so a glyph added to an updater message is still
|
|
25
|
+
# checked against a stand-in — tests/test_console_encoding.py.
|
|
26
|
+
from sdk.platform_compat import _ASCII_FALLBACK # noqa: F401
|
|
27
|
+
from sdk.platform_compat import safe_print as _say
|
|
21
28
|
|
|
22
29
|
_DIST = "devops-bot-sdk"
|
|
23
30
|
_EXTRAS = "[ui]" # control-panel deps (fastapi/uvicorn/starlette) — keep on every self-update
|
|
@@ -26,27 +33,6 @@ _GITHUB_SSH = f"git+ssh://git@github.com/{_GITHUB_REPO}.git"
|
|
|
26
33
|
_PYPI_JSON = f"https://pypi.org/pypi/{_DIST}/json"
|
|
27
34
|
|
|
28
35
|
|
|
29
|
-
#: Non-cp1252 glyphs this module prints → an ASCII stand-in. See :func:`_say`.
|
|
30
|
-
_ASCII_FALLBACK = str.maketrans({"✓": "OK", "→": "->", "↻": "~", "⚠": "!", "…": "..."})
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _say(message: str) -> None:
|
|
34
|
-
"""``print`` that can never take the updater down on a legacy Windows codepage.
|
|
35
|
-
|
|
36
|
-
The real fix is ``platform_compat.ensure_utf8_stdio``, which every entry point
|
|
37
|
-
installs. This is the backstop for the one module that must not be allowed to
|
|
38
|
-
die: on the PyPI path the ``updating … → …`` banner prints BEFORE
|
|
39
|
-
``self_update()`` runs, so a stdout that cannot encode ``→`` would deadlock the
|
|
40
|
-
updater against its own repair — the crash would recur on every single run,
|
|
41
|
-
and the release that fixes it could never install itself. Degrading to ASCII
|
|
42
|
-
keeps that from ever being possible again.
|
|
43
|
-
"""
|
|
44
|
-
try:
|
|
45
|
-
print(message)
|
|
46
|
-
except UnicodeEncodeError:
|
|
47
|
-
print(message.translate(_ASCII_FALLBACK))
|
|
48
|
-
|
|
49
|
-
|
|
50
36
|
def _with_extras(target: str) -> str:
|
|
51
37
|
"""Attach the ``[ui]`` extra so a self-update never drops the control-panel
|
|
52
38
|
deps — the reason ``agentos-ui`` broke (``No module named 'starlette'``) after
|
|
@@ -243,18 +229,79 @@ def _install_candidates() -> list[str]:
|
|
|
243
229
|
return _unique([_with_extras(c) for c in cands])
|
|
244
230
|
|
|
245
231
|
|
|
246
|
-
def
|
|
247
|
-
"""pip
|
|
248
|
-
|
|
232
|
+
def _redact(text: str) -> str:
|
|
233
|
+
"""Strip credentials out of pip output before it is printed or logged.
|
|
234
|
+
|
|
235
|
+
One install candidate is ``git+https://<token>@github.com/...`` built from the
|
|
236
|
+
user's GitHub OAuth token, and pip echoes the URL it failed on. Reporting pip
|
|
237
|
+
failures (which this module now does) must not write that token into
|
|
238
|
+
``~/.agentos/auto.log`` or the panel's captured output.
|
|
239
|
+
"""
|
|
240
|
+
import re
|
|
241
|
+
return re.sub(r"(https?://)[^/\s:@]+(?::[^/\s@]*)?@", r"\1***@", text)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _pip_install(
|
|
245
|
+
url: str, *, force: bool = False, no_deps: bool | None = None,
|
|
246
|
+
no_cache: bool = False,
|
|
247
|
+
) -> tuple[bool, str]:
|
|
248
|
+
"""``pip install --upgrade <url>``. Returns (exited-0, redacted output).
|
|
249
|
+
|
|
250
|
+
The output is returned rather than discarded because "pip exited non-zero" is
|
|
251
|
+
the single most useful thing to know about a box that will not update, and it
|
|
252
|
+
used to be swallowed whole — leaving a silent no-op that read as success.
|
|
253
|
+
|
|
254
|
+
``force`` adds ``--force-reinstall`` (defaulting ``no_deps`` on, for the
|
|
255
|
+
corrupt-install repair path, which wants to replace our own files and nothing
|
|
256
|
+
else). ``no_cache`` bypasses pip's wheel cache — a cached wheel built from an
|
|
257
|
+
earlier clone of the same git ref is one of the ways an upgrade silently
|
|
258
|
+
installs the version it already had.
|
|
259
|
+
"""
|
|
260
|
+
if no_deps is None:
|
|
261
|
+
no_deps = force
|
|
262
|
+
extra: list[str] = []
|
|
263
|
+
if force:
|
|
264
|
+
extra.append("--force-reinstall")
|
|
265
|
+
if no_deps:
|
|
266
|
+
extra.append("--no-deps")
|
|
267
|
+
if no_cache:
|
|
268
|
+
extra.append("--no-cache-dir")
|
|
249
269
|
try:
|
|
250
|
-
subprocess.run(
|
|
270
|
+
proc = subprocess.run(
|
|
251
271
|
[sys.executable, "-m", "pip", "install", "--upgrade", "--quiet",
|
|
252
272
|
*extra, *_pip_flags(), url],
|
|
253
|
-
|
|
273
|
+
capture_output=True, text=True, errors="replace", timeout=900,
|
|
274
|
+
check=False, # the exit code is the answer, not an exception
|
|
254
275
|
)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
276
|
+
out = _redact(((proc.stderr or "") + (proc.stdout or "")).strip())
|
|
277
|
+
return (proc.returncode == 0, out)
|
|
278
|
+
except Exception as exc: # noqa: BLE001 — pip missing, timeout, unreadable env
|
|
279
|
+
return (False, _redact(str(exc)))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def installed_version() -> str:
|
|
283
|
+
"""The version pip has ON DISK, read in a FRESH interpreter.
|
|
284
|
+
|
|
285
|
+
:func:`current_version` answers from THIS process, and after a pip upgrade
|
|
286
|
+
that answer cannot be trusted: ``importlib.metadata`` caches its path scan,
|
|
287
|
+
and its ``sdk.__version__`` fallback is a module that was imported before the
|
|
288
|
+
upgrade ran. Comparing a post-upgrade reading against a pre-upgrade one is
|
|
289
|
+
exactly the check that decides whether we say "updated" or "already up to
|
|
290
|
+
date", so it has to come from a process that started after pip finished.
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
r = subprocess.run(
|
|
294
|
+
[sys.executable, "-c",
|
|
295
|
+
f"from importlib.metadata import version; print(version({_DIST!r}))"],
|
|
296
|
+
capture_output=True, text=True, errors="replace", timeout=60,
|
|
297
|
+
check=False,
|
|
298
|
+
)
|
|
299
|
+
ver = (r.stdout or "").strip().splitlines()
|
|
300
|
+
if r.returncode == 0 and ver and ver[-1].strip():
|
|
301
|
+
return ver[-1].strip()
|
|
302
|
+
except Exception: # noqa: BLE001,S110 — an unreadable child is not fatal here
|
|
303
|
+
pass
|
|
304
|
+
return current_version()
|
|
258
305
|
|
|
259
306
|
|
|
260
307
|
def _verify_install() -> bool:
|
|
@@ -277,21 +324,106 @@ def _verify_install() -> bool:
|
|
|
277
324
|
def _repair_install() -> bool:
|
|
278
325
|
"""Force-reinstall to recover from a corrupt upgrade; verifies the result."""
|
|
279
326
|
for url in _install_candidates():
|
|
280
|
-
|
|
327
|
+
ok, _out = _pip_install(url, force=True)
|
|
328
|
+
if ok and _verify_install():
|
|
281
329
|
return True
|
|
282
330
|
return False
|
|
283
331
|
|
|
284
332
|
|
|
333
|
+
def _source_label(url: str) -> str:
|
|
334
|
+
"""A short, token-free name for an install candidate, for failure messages."""
|
|
335
|
+
if "ssh://" in url:
|
|
336
|
+
return "git+ssh"
|
|
337
|
+
if "https://" in url:
|
|
338
|
+
return "git+https"
|
|
339
|
+
return "pypi"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _tail(text: str, lines: int = 2, width: int = 300) -> str:
|
|
343
|
+
"""The last few lines of pip output — enough to name the cause, short enough
|
|
344
|
+
to sit in a log line."""
|
|
345
|
+
kept = [ln.strip() for ln in (text or "").splitlines() if ln.strip()][-lines:]
|
|
346
|
+
joined = " / ".join(kept)
|
|
347
|
+
return joined[:width] + ("…" if len(joined) > width else "")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class UpgradeResult(NamedTuple):
|
|
351
|
+
"""What an upgrade attempt actually did.
|
|
352
|
+
|
|
353
|
+
``changed`` is the only claim worth making to a user: it is measured from the
|
|
354
|
+
version on disk before and after, not inferred from pip's exit code.
|
|
355
|
+
"""
|
|
356
|
+
changed: bool # the installed version really moved
|
|
357
|
+
before: str # version on disk before the attempt
|
|
358
|
+
after: str # version on disk after it
|
|
359
|
+
ran: bool # at least one candidate's pip exited 0
|
|
360
|
+
detail: str # why nothing moved / why every candidate failed
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def upgrade(target: str | None = None) -> UpgradeResult:
|
|
364
|
+
"""Upgrade the installed SDK via pip, and VERIFY that it happened.
|
|
365
|
+
|
|
366
|
+
Tries the candidates in order (SSH → detected URL → token-HTTPS → PyPI) and,
|
|
367
|
+
after each pip that exits 0, re-reads the version from disk in a fresh
|
|
368
|
+
interpreter. A candidate that exits 0 without moving the version is a no-op,
|
|
369
|
+
not a success, so the walk continues to the next one instead of stopping
|
|
370
|
+
there.
|
|
371
|
+
|
|
372
|
+
That distinction is the whole point of this function. Treating pip's exit code
|
|
373
|
+
as the answer is what let a box sit for weeks on an ancient SDK while printing
|
|
374
|
+
"✓ already up to date" on every run: a git candidate resolved to a wheel it had
|
|
375
|
+
already installed, exited 0, short-circuited the candidate list before the PyPI
|
|
376
|
+
fallback was ever tried, and the unchanged version read as "current".
|
|
377
|
+
|
|
378
|
+
``target`` is the version we know is published (from PyPI). When it is given
|
|
379
|
+
and the ordinary pass does not reach it, a second forced pass runs with
|
|
380
|
+
``--force-reinstall --no-cache-dir`` — the escape hatch for a stale wheel
|
|
381
|
+
cache or a same-version direct reference pip decides to skip. Without it a
|
|
382
|
+
wedged box can never recover on its own, which is the state this repair is for.
|
|
383
|
+
"""
|
|
384
|
+
before = installed_version()
|
|
385
|
+
candidates = _install_candidates() # resolved once: it costs a token fetch
|
|
386
|
+
ran = False
|
|
387
|
+
notes: list[str] = []
|
|
388
|
+
|
|
389
|
+
def _reached(after: str) -> bool:
|
|
390
|
+
return bool(target) and _vtuple(after) >= _vtuple(target)
|
|
391
|
+
|
|
392
|
+
for forced in (False, True):
|
|
393
|
+
if forced and (not target or _reached(installed_version())):
|
|
394
|
+
break # nothing published beyond us, or we already got there
|
|
395
|
+
for url in candidates:
|
|
396
|
+
ok, out = _pip_install(url, force=forced, no_deps=False, no_cache=forced)
|
|
397
|
+
label = _source_label(url) + (" (forced)" if forced else "")
|
|
398
|
+
if not ok:
|
|
399
|
+
notes.append(f"{label}: {_tail(out)}")
|
|
400
|
+
continue
|
|
401
|
+
ran = True
|
|
402
|
+
after = installed_version()
|
|
403
|
+
if _vtuple(after) != _vtuple(before) or _reached(after):
|
|
404
|
+
return UpgradeResult(after != before, before, after, True, "")
|
|
405
|
+
if not target:
|
|
406
|
+
# pip exited 0 and the version did not move. With no published
|
|
407
|
+
# version to check against, "no-op" and "already current" are the
|
|
408
|
+
# same observation — so stop here rather than reinstalling from
|
|
409
|
+
# three more candidates on every routine run.
|
|
410
|
+
return UpgradeResult(False, before, after, True, "")
|
|
411
|
+
notes.append(f"{label}: pip exited 0 but the installed version "
|
|
412
|
+
f"stayed {after}")
|
|
413
|
+
|
|
414
|
+
after = installed_version()
|
|
415
|
+
return UpgradeResult(after != before, before, after, ran, "; ".join(notes))
|
|
416
|
+
|
|
417
|
+
|
|
285
418
|
def self_update() -> bool:
|
|
286
|
-
"""Upgrade the installed SDK via pip.
|
|
419
|
+
"""Upgrade the installed SDK via pip. True only if the version really moved.
|
|
287
420
|
|
|
288
|
-
|
|
289
|
-
|
|
421
|
+
Thin wrapper over :func:`upgrade` — note that a box already on the newest
|
|
422
|
+
version returns False here (nothing changed), which is why callers that need
|
|
423
|
+
to tell "already current" from "the upgrade did not take" use ``upgrade()``
|
|
424
|
+
and read :attr:`UpgradeResult.after` instead of this boolean.
|
|
290
425
|
"""
|
|
291
|
-
|
|
292
|
-
if _pip_install(url):
|
|
293
|
-
return True
|
|
294
|
-
return False
|
|
426
|
+
return upgrade().changed
|
|
295
427
|
|
|
296
428
|
|
|
297
429
|
def _ensure_service(quiet: bool = False, *, restart: bool = False) -> None:
|
|
@@ -367,9 +499,15 @@ def _ensure_firewall(quiet: bool = False) -> None:
|
|
|
367
499
|
def auto_update(quiet: bool = False) -> tuple[bool, str, str | None]:
|
|
368
500
|
"""Check for a newer version and upgrade if one exists.
|
|
369
501
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
502
|
+
PyPI's published version is fetched on BOTH install paths, and not only to
|
|
503
|
+
skip needless work: it is the one independent fact available about whether
|
|
504
|
+
this box is current. A git install can legitimately be ahead of PyPI, so a
|
|
505
|
+
published version that is merely equal or older proves nothing — but a
|
|
506
|
+
published version STRICTLY NEWER than what is on disk after an upgrade
|
|
507
|
+
attempt proves the upgrade did not take, no matter what pip's exit code said.
|
|
508
|
+
That case is now reported (with pip's own reason) instead of being announced
|
|
509
|
+
as "✓ already up to date", and :func:`upgrade` has already tried a forced,
|
|
510
|
+
cache-bypassing reinstall to fix it.
|
|
373
511
|
|
|
374
512
|
Returns (updated, version_before, version_after_or_none).
|
|
375
513
|
No-op when AGENTOS_NO_UPDATE=1 or already up to date.
|
|
@@ -378,40 +516,62 @@ def auto_update(quiet: bool = False) -> tuple[bool, str, str | None]:
|
|
|
378
516
|
if os.getenv("AGENTOS_NO_UPDATE"):
|
|
379
517
|
return (False, cur, None)
|
|
380
518
|
|
|
519
|
+
latest = latest_pypi_version()
|
|
381
520
|
is_github = _detect_install_url().startswith("git+")
|
|
521
|
+
behind_pypi = bool(latest) and _vtuple(latest) > _vtuple(cur)
|
|
382
522
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
_ensure_firewall(quiet)
|
|
388
|
-
return (False, cur, latest)
|
|
389
|
-
if not quiet:
|
|
390
|
-
_say(f"updating {_DIST} {cur} → {latest} …")
|
|
391
|
-
ok = self_update()
|
|
392
|
-
if not quiet and ok:
|
|
393
|
-
_say("✓ updated.")
|
|
394
|
-
# Ensure the service exists + runs the new code (restart only on a real
|
|
395
|
-
# upgrade); also self-heals a missing/down service when already current.
|
|
396
|
-
_ensure_service(quiet, restart=ok)
|
|
523
|
+
# Nothing to do on a PyPI install that is already at (or past) the published
|
|
524
|
+
# version — a git install still checks its branch, which PyPI cannot answer for.
|
|
525
|
+
if not is_github and not behind_pypi:
|
|
526
|
+
_ensure_service(quiet, restart=False) # already current — just self-heal the service
|
|
397
527
|
_ensure_firewall(quiet)
|
|
398
|
-
return (
|
|
528
|
+
return (False, cur, latest)
|
|
399
529
|
|
|
400
|
-
# GitHub path
|
|
401
530
|
if not quiet:
|
|
402
|
-
_say("
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
531
|
+
_say(f"updating {_DIST} {cur} → {latest} …" if behind_pypi
|
|
532
|
+
else "checking for SDK updates …")
|
|
533
|
+
|
|
534
|
+
result = upgrade(target=latest if behind_pypi else None)
|
|
406
535
|
if not quiet:
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
_ensure_service(quiet, restart=updated)
|
|
536
|
+
_report(result, latest)
|
|
537
|
+
|
|
538
|
+
# Ensure the service exists + runs the new code (restart only on a real
|
|
539
|
+
# upgrade); also self-heals a missing/down service when already current.
|
|
540
|
+
_ensure_service(quiet, restart=result.changed)
|
|
413
541
|
_ensure_firewall(quiet)
|
|
414
|
-
return (
|
|
542
|
+
return (result.changed, cur, result.after if result.changed else None)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _report(result: UpgradeResult, latest: str | None) -> None:
|
|
546
|
+
"""Say what the upgrade attempt actually did.
|
|
547
|
+
|
|
548
|
+
The silent case is deliberate and narrow: on a git install with nothing newer
|
|
549
|
+
published, an upgrade that changed nothing is the ordinary state and we still
|
|
550
|
+
have no way to see the branch, so "already up to date (v…)" is the best claim
|
|
551
|
+
available. Everything else is either a real upgrade or a real problem, and a
|
|
552
|
+
problem the operator can act on beats a green tick they cannot trust.
|
|
553
|
+
"""
|
|
554
|
+
if result.changed:
|
|
555
|
+
_say(f"✓ updated {result.before} → {result.after}.")
|
|
556
|
+
return
|
|
557
|
+
|
|
558
|
+
if latest and _vtuple(latest) > _vtuple(result.after):
|
|
559
|
+
_say(f"⚠ still on v{result.after} — v{latest} is published but the "
|
|
560
|
+
f"upgrade did not take.")
|
|
561
|
+
if result.detail:
|
|
562
|
+
_say(f" pip said: {result.detail}")
|
|
563
|
+
_say(" fix it by hand with:\n"
|
|
564
|
+
f' "{sys.executable}" -m pip install --upgrade --force-reinstall '
|
|
565
|
+
f'--no-cache-dir "{_DIST}{_EXTRAS}"')
|
|
566
|
+
return
|
|
567
|
+
|
|
568
|
+
if result.ran:
|
|
569
|
+
_say(f"✓ already up to date (v{result.after}).")
|
|
570
|
+
elif result.detail:
|
|
571
|
+
# Every candidate failed: no SSH key, no token, no network. Previously
|
|
572
|
+
# silent, which is indistinguishable from a successful check.
|
|
573
|
+
_say(f"⚠ could not check for updates (staying on v{result.after}).")
|
|
574
|
+
_say(f" pip said: {result.detail}")
|
|
415
575
|
|
|
416
576
|
|
|
417
577
|
def auto_update_and_reexec(module: str, argv: list[str]) -> None:
|
|
@@ -13,6 +13,7 @@ Two gaps this covers:
|
|
|
13
13
|
"""
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
+
import pytest
|
|
16
17
|
from unittest.mock import patch
|
|
17
18
|
|
|
18
19
|
from sdk import firewall, updater
|
|
@@ -159,12 +160,45 @@ class TestEnsureOpen:
|
|
|
159
160
|
|
|
160
161
|
# ── The post-update hook ──────────────────────────────────────────────
|
|
161
162
|
class TestEnsureFirewallHook:
|
|
162
|
-
|
|
163
|
-
"
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
163
|
+
@pytest.mark.parametrize(
|
|
164
|
+
"install_url, pypi_latest, result",
|
|
165
|
+
[
|
|
166
|
+
# PyPI install, nothing newer published — the early return.
|
|
167
|
+
("devops-bot-sdk", "1.0.0", None),
|
|
168
|
+
# An upgrade that took.
|
|
169
|
+
("git+ssh://git@github.com/x/y.git", "1.1.0",
|
|
170
|
+
updater.UpgradeResult(True, "1.0.0", "1.1.0", True, "")),
|
|
171
|
+
# An upgrade that did NOT take — the case that used to report success.
|
|
172
|
+
("git+ssh://git@github.com/x/y.git", "1.1.0",
|
|
173
|
+
updater.UpgradeResult(False, "1.0.0", "1.0.0", True, "pip no-op")),
|
|
174
|
+
],
|
|
175
|
+
)
|
|
176
|
+
def test_it_runs_on_every_auto_update_path(self, install_url, pypi_latest, result):
|
|
177
|
+
"""Every way out of `auto_update` must have checked the firewall first.
|
|
178
|
+
|
|
179
|
+
Driven through the function rather than counted in its source: the number
|
|
180
|
+
of return paths is an implementation detail (merging the PyPI and GitHub
|
|
181
|
+
branches changed it), while "no outcome skips the check" is the property
|
|
182
|
+
that keeps a host with a shut port from being reported healthy forever.
|
|
183
|
+
"""
|
|
184
|
+
calls: list[bool] = []
|
|
185
|
+
with patch.object(updater, "current_version", return_value="1.0.0"), \
|
|
186
|
+
patch.object(updater, "latest_pypi_version", return_value=pypi_latest), \
|
|
187
|
+
patch.object(updater, "_detect_install_url", return_value=install_url), \
|
|
188
|
+
patch.object(updater, "upgrade", return_value=result), \
|
|
189
|
+
patch.object(updater, "_ensure_service", lambda *a, **k: None), \
|
|
190
|
+
patch.object(updater, "_ensure_firewall", lambda *a, **k: calls.append(True)):
|
|
191
|
+
updater.auto_update(quiet=True)
|
|
192
|
+
assert calls, "auto_update returned without checking the firewall"
|
|
193
|
+
|
|
194
|
+
def test_the_opt_out_return_checks_nothing(self):
|
|
195
|
+
"""AGENTOS_NO_UPDATE is the one exemption: it returns before the updater
|
|
196
|
+
does anything at all."""
|
|
197
|
+
calls: list[bool] = []
|
|
198
|
+
with patch.dict("os.environ", {"AGENTOS_NO_UPDATE": "1"}), \
|
|
199
|
+
patch.object(updater, "_ensure_firewall", lambda *a, **k: calls.append(True)):
|
|
200
|
+
updater.auto_update(quiet=True)
|
|
201
|
+
assert not calls
|
|
168
202
|
|
|
169
203
|
def test_opt_out_env_var_skips_it(self):
|
|
170
204
|
with patch.dict("os.environ", {"AGENTOS_NO_FIREWALL": "1"}), \
|