devops-bot-sdk 1.6.69__tar.gz → 1.6.70__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.69 → devops_bot_sdk-1.6.70}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/os_setup.py +5 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/shipper.py +46 -1
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/activity.py +1 -1
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/client.py +39 -8
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/vps_health.py +9 -1
- devops_bot_sdk-1.6.70/sdk/vps_identity.py +174 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_vps_health.py +9 -0
- devops_bot_sdk-1.6.70/tests/test_vps_only_reporting.py +234 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/README.md +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/classifier.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/common.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/linux_auditd.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/macos_eslogger.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/windows_eventlog.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/models.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/recorder.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/redact.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/settings.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/spool.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/schedules.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/ticket_run.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/browser_profile.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/custom_agents.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/departments.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/estimator.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/firewall.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/formats.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/github_fetch.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/graph_recon.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/intake.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/journal.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/memory_guard.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/phase_timer.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/plan_run.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/prd_progress.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/proof_readers.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/providers.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/resume.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/router.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/schedule_tickets.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/scheduler.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/self_report.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/self_report_local.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/swagger.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/task_file.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/task_types.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/tickets.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/tiers/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/tiers/mini_agent.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/chat.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/report.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/schedules.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/swagger.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/url_access.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/write_hook.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_activity_collectors.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_activity_core.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_activity_macos.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_activity_os_setup.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_activity_pipeline.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_activity_tracking.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_crud_cli.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_run_ticket.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_schedules.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_workspace_settings.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_allowed_tools_config.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_auto_agent_dispatch.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_browser_profile.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_cheap_tier.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_client_jira_projects.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_config_token_diagnostics.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_configure_token_owner.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_console_encoding.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_crash_recovery.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_departments.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_done_transition.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_epic_decompose_run.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_epic_inherited_agent_id.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_epic_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_estimator.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_fallback_cli.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_firewall_open_on_update.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_formats.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_github_fetch.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_graph_recon.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_graphify_build.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_graphify_hook_install.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_intake_job_outcome.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_intake_plan_review.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_journal.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_local_exec_provider.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_mcp_route_contract.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_memory_guard.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_phase_timer.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_plan_new_command.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_plan_run_activity.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_pr_identity.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_prd_artifact.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_project_path_resolution.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_proof_readers.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_providers.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_recon_session_isolation.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_resume.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_router.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_run_auto_agent_only.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_run_outputs.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_run_verdict_is_honest.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_schedule_api.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_schedule_outputs_and_plans.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_schedule_tickets.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_scheduler.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_scheduler_transient_retry.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_self_report.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_service_token_warning.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_stale_panel_restart.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_swagger_command.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_task_file_and_plan_run.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_task_file_format_doc.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ticket_cli.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ticket_fields.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ticket_run_resolve_by_id.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ticket_speed_fixes.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_tier_flag_parity.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ui_gate.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ui_lifespan.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_update_timer.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_updater_pep668.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_updater_reports_the_truth.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_url_access.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_windows_loop_identity.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_windows_task_paths.py +0 -0
- {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_write_hook.py +0 -0
|
@@ -53,6 +53,7 @@ sdk/tickets.py
|
|
|
53
53
|
sdk/updater.py
|
|
54
54
|
sdk/url_access.py
|
|
55
55
|
sdk/vps_health.py
|
|
56
|
+
sdk/vps_identity.py
|
|
56
57
|
sdk/write_hook.py
|
|
57
58
|
sdk/activity/__init__.py
|
|
58
59
|
sdk/activity/classifier.py
|
|
@@ -226,6 +227,7 @@ tests/test_updater_pep668.py
|
|
|
226
227
|
tests/test_updater_reports_the_truth.py
|
|
227
228
|
tests/test_url_access.py
|
|
228
229
|
tests/test_vps_health.py
|
|
230
|
+
tests/test_vps_only_reporting.py
|
|
229
231
|
tests/test_windows_loop_identity.py
|
|
230
232
|
tests/test_windows_task_paths.py
|
|
231
233
|
tests/test_write_hook.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.6.
|
|
3
|
+
Version: 1.6.70
|
|
4
4
|
|
|
5
5
|
Public surface:
|
|
6
6
|
BackendClient.from_config() — create client from ~/.agentos/config.toml
|
|
@@ -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.70" # 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
|
|
@@ -188,6 +188,11 @@ def _flag_value() -> str:
|
|
|
188
188
|
return value.strip().strip('"').strip("'")
|
|
189
189
|
except OSError:
|
|
190
190
|
pass
|
|
191
|
+
# Under sudo, settings.setting reads ROOT's .env. Falling back to it made a
|
|
192
|
+
# flag left in /root/.agentos/.env answer for the panel user, so _enable_flag
|
|
193
|
+
# never wrote theirs and the collector never started — on every restart.
|
|
194
|
+
if _is_privileged() and _panel_user() is not None:
|
|
195
|
+
return ""
|
|
191
196
|
return settings.setting(AUDIT_FLAG)
|
|
192
197
|
|
|
193
198
|
|
|
@@ -176,12 +176,57 @@ async def flush_once(
|
|
|
176
176
|
return result
|
|
177
177
|
|
|
178
178
|
|
|
179
|
+
def discard_spool(spool: Spool | None = None) -> int:
|
|
180
|
+
"""Delete everything queued, without sending it. Returns the rows dropped."""
|
|
181
|
+
spool = spool or settings.get_spool()
|
|
182
|
+
dropped = 0
|
|
183
|
+
while True:
|
|
184
|
+
chunk = spool.next_chunk()
|
|
185
|
+
if not chunk:
|
|
186
|
+
return dropped
|
|
187
|
+
dropped += len(chunk)
|
|
188
|
+
spool.ack(chunk)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
async def ship_tick(
|
|
192
|
+
is_vps: Callable[[], Awaitable[bool | None]] | None = None,
|
|
193
|
+
) -> FlushResult | None:
|
|
194
|
+
"""One cron tick: ship only from the caller's assigned VPS.
|
|
195
|
+
|
|
196
|
+
The relay files every batch under the TOKEN's VPS, so a laptop running the SDK
|
|
197
|
+
with the same token put its own events in the VPS's audit trail. On a machine
|
|
198
|
+
that is definitely not the VPS the queue is discarded — kept, it would only grow
|
|
199
|
+
forever, since that machine will never be allowed to send it. When the answer
|
|
200
|
+
is unknown (profile unreachable) everything is kept for the next tick.
|
|
201
|
+
"""
|
|
202
|
+
if is_vps is None:
|
|
203
|
+
from sdk import vps_identity
|
|
204
|
+
is_vps = vps_identity.is_assigned_vps
|
|
205
|
+
try:
|
|
206
|
+
verdict = await is_vps()
|
|
207
|
+
except Exception: # noqa: BLE001 — is_assigned_vps never raises, but be certain
|
|
208
|
+
verdict = None
|
|
209
|
+
if verdict is None:
|
|
210
|
+
return None
|
|
211
|
+
if verdict is False:
|
|
212
|
+
try:
|
|
213
|
+
dropped = discard_spool()
|
|
214
|
+
except Exception as exc: # noqa: BLE001
|
|
215
|
+
logger.warning("activity: could not discard the spool: %s", exc)
|
|
216
|
+
return None
|
|
217
|
+
if dropped:
|
|
218
|
+
logger.info("activity.not_assigned_vps discarded=%d (this machine is not the "
|
|
219
|
+
"user's VPS; nothing is shipped from it)", dropped)
|
|
220
|
+
return None
|
|
221
|
+
return await flush_once()
|
|
222
|
+
|
|
223
|
+
|
|
179
224
|
async def run_forever(interval_s: float | None = None, *, initial_delay_s: float = 30.0) -> None:
|
|
180
225
|
"""Flush every ``interval_s`` (default 5 minutes) until cancelled."""
|
|
181
226
|
await asyncio.sleep(initial_delay_s)
|
|
182
227
|
while True:
|
|
183
228
|
try:
|
|
184
|
-
await
|
|
229
|
+
await ship_tick()
|
|
185
230
|
every = interval_s if interval_s is not None else settings.ship_interval_seconds()
|
|
186
231
|
await asyncio.sleep(every)
|
|
187
232
|
except asyncio.CancelledError:
|
|
@@ -79,7 +79,7 @@ def describe(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
|
79
79
|
# per-run event. So an agent run costs one POST no matter how chatty it is.
|
|
80
80
|
_LINE_BUF: dict[str, list[str]] = {}
|
|
81
81
|
_LINE_BUF_MAX_LINES = 40 # keep the tail — recent lines are the useful ones
|
|
82
|
-
_LINE_BUF_MAX_CHARS = 4000 #
|
|
82
|
+
_LINE_BUF_MAX_CHARS = 4000 # trimmed to the gateway limit by client._fit_activity
|
|
83
83
|
|
|
84
84
|
|
|
85
85
|
def note(bucket: str, text: str) -> None:
|
|
@@ -121,9 +121,19 @@ def _notify_status(task_id: str, status: str) -> None:
|
|
|
121
121
|
# `logger.info` line regardless of whether it buffers or POSTs.
|
|
122
122
|
_ACTIVITY_FLUSH_MIN_S = float(os.getenv("AGENTOS_ACTIVITY_FLUSH_MIN_S", "") or 180.0)
|
|
123
123
|
_ACTIVITY_FLUSH_MAX_S = float(os.getenv("AGENTOS_ACTIVITY_FLUSH_MAX_S", "") or 300.0)
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
|
|
124
|
+
# The gateway rejects an `activity` over 2000 characters with a 400 ("Activity
|
|
125
|
+
# must be shorter than or equal to 2000 characters"). The cap used to be 8000, so
|
|
126
|
+
# every coalesced flush and every long scheduled-run summary was refused and the
|
|
127
|
+
# run left no trace in agent_activities. Keep the tail: the most recent lines are
|
|
128
|
+
# the most useful.
|
|
129
|
+
_ACTIVITY_MAX_CHARS = 2000
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _fit_activity(text: str) -> str:
|
|
133
|
+
"""``text`` cut to the gateway's limit, keeping the tail behind a ``…`` marker."""
|
|
134
|
+
if len(text) <= _ACTIVITY_MAX_CHARS:
|
|
135
|
+
return text
|
|
136
|
+
return "…\n" + text[-(_ACTIVITY_MAX_CHARS - 2):]
|
|
127
137
|
|
|
128
138
|
|
|
129
139
|
class ActivityFlusher:
|
|
@@ -167,9 +177,7 @@ class ActivityFlusher:
|
|
|
167
177
|
if not self._buf:
|
|
168
178
|
return # nothing pending → don't open a connection at all
|
|
169
179
|
lines, self._buf = self._buf, [] # swap out atomically (no await between)
|
|
170
|
-
body = "\n".join(lines)
|
|
171
|
-
if len(body) > _ACTIVITY_FLUSH_MAX_CHARS:
|
|
172
|
-
body = "…\n" + body[-_ACTIVITY_FLUSH_MAX_CHARS:]
|
|
180
|
+
body = _fit_activity("\n".join(lines))
|
|
173
181
|
try:
|
|
174
182
|
await self._client.log_activity(body, task_id=self._task_id, immediate=True)
|
|
175
183
|
except Exception as exc: # noqa: BLE001 — never let a flush break a run
|
|
@@ -313,6 +321,29 @@ class BackendClient:
|
|
|
313
321
|
except Exception as exc:
|
|
314
322
|
raise BackendUnreachable(self._base_url, str(exc)) from exc
|
|
315
323
|
|
|
324
|
+
async def assigned_vps_ip(self) -> str:
|
|
325
|
+
"""The IP of the caller's assigned VPS from ``GET /ml-api/users/profile``.
|
|
326
|
+
|
|
327
|
+
``""`` when the profile names no VPS. Raises BackendUnreachable /
|
|
328
|
+
BackendAuthFailed — the caller (``sdk.vps_identity``) turns those into
|
|
329
|
+
"unknown" rather than "not a VPS".
|
|
330
|
+
"""
|
|
331
|
+
from sdk.config import BE_DEFAULT_URL
|
|
332
|
+
from sdk.vps_identity import extract_vps_ip
|
|
333
|
+
|
|
334
|
+
url = f"{BE_DEFAULT_URL.rstrip('/')}/ml-api/users/profile"
|
|
335
|
+
headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
|
|
336
|
+
try:
|
|
337
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
338
|
+
resp = await client.get(url, headers=headers)
|
|
339
|
+
_check_status(resp.status_code, url)
|
|
340
|
+
data = (resp.json() or {}).get("data") or {}
|
|
341
|
+
except (BackendAuthFailed, BackendVersionTooOld):
|
|
342
|
+
raise
|
|
343
|
+
except Exception as exc:
|
|
344
|
+
raise BackendUnreachable(url, str(exc)) from exc
|
|
345
|
+
return extract_vps_ip(data.get("user") or {})
|
|
346
|
+
|
|
316
347
|
async def profile(self) -> UserProfile:
|
|
317
348
|
"""GET {BE}/ml-api/users/profile — the caller's role + department.
|
|
318
349
|
|
|
@@ -1766,7 +1797,7 @@ class BackendClient:
|
|
|
1766
1797
|
"developer-name": "null",
|
|
1767
1798
|
"product": "",
|
|
1768
1799
|
}
|
|
1769
|
-
body: dict[str, str] = {"activity": str(activity)}
|
|
1800
|
+
body: dict[str, str] = {"activity": _fit_activity(str(activity))}
|
|
1770
1801
|
tid = str(task_id or "").strip()
|
|
1771
1802
|
if tid:
|
|
1772
1803
|
body["taskId"] = tid
|
|
@@ -1800,7 +1831,7 @@ class BackendClient:
|
|
|
1800
1831
|
return False
|
|
1801
1832
|
url = f"{ML_GATEWAY_URL.rstrip('/')}/ml-api/agent-activity"
|
|
1802
1833
|
headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
|
|
1803
|
-
body: dict[str, str] = {"activity": str(activity), "agentId": aid}
|
|
1834
|
+
body: dict[str, str] = {"activity": _fit_activity(str(activity)), "agentId": aid}
|
|
1804
1835
|
tid = str(task_id or "").strip()
|
|
1805
1836
|
if tid:
|
|
1806
1837
|
body["taskId"] = tid
|
|
@@ -211,10 +211,18 @@ async def report_once(client: "BackendClient | None" = None) -> bool:
|
|
|
211
211
|
"""Collect + PATCH one health snapshot. Returns True on a 2xx. Never raises."""
|
|
212
212
|
payload: dict[str, Any] = {}
|
|
213
213
|
try:
|
|
214
|
-
payload = build_payload()
|
|
215
214
|
if client is None:
|
|
216
215
|
from sdk.client import BackendClient
|
|
217
216
|
client = BackendClient.from_config()
|
|
217
|
+
# Only the assigned VPS may write the VPS row — a laptop running the SDK
|
|
218
|
+
# with the same token used to overwrite it (see sdk.vps_identity).
|
|
219
|
+
from sdk import vps_identity
|
|
220
|
+
verdict = await vps_identity.is_assigned_vps(client)
|
|
221
|
+
if verdict is not True:
|
|
222
|
+
reason = "not_assigned_vps" if verdict is False else "vps_identity_unknown"
|
|
223
|
+
_LAST.update({"at": _now_iso_ms(), "ok": False, "reason": reason, "payload": {}})
|
|
224
|
+
return False
|
|
225
|
+
payload = build_payload()
|
|
218
226
|
ok = await client.report_vps_allocation(payload)
|
|
219
227
|
_LAST.update({
|
|
220
228
|
"at": payload.get("lastPingedAt", ""),
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Is THIS machine the caller's assigned VPS?
|
|
2
|
+
|
|
3
|
+
Health pings (``sdk.vps_health``) and the activity trail (``sdk.activity.shipper``)
|
|
4
|
+
both describe "your VPS", and both are keyed SERVER-side on the ``co_`` token — one
|
|
5
|
+
``contabo_allocation`` row per user. So every machine that ran the SDK with that
|
|
6
|
+
token reported as the VPS: a laptop's CPU, RAM, disk and uptime overwrote the VPS
|
|
7
|
+
row every two hours, and the laptop's events landed in the VPS's audit trail. A
|
|
8
|
+
user with the SDK on a laptop and a VPS had a "VPS" whose boot time was the
|
|
9
|
+
laptop's.
|
|
10
|
+
|
|
11
|
+
The answer comes from comparing the IP the platform has on record for the user's
|
|
12
|
+
VPS (``GET /ml-api/users/profile`` → ``vps.ipAddress``, the field the backend's
|
|
13
|
+
proxy already trusts) with this machine's own addresses. Nothing leaves the
|
|
14
|
+
machine to find those — interface addresses plus the route-to-internet address —
|
|
15
|
+
so the SDK's one-egress-path rule holds.
|
|
16
|
+
|
|
17
|
+
Three answers, and the callers treat them differently:
|
|
18
|
+
``True`` this is the assigned VPS — report.
|
|
19
|
+
``False`` it is not (or the user has no VPS) — do not report.
|
|
20
|
+
``None`` unknown (profile unreachable) — skip this tick, ask again next tick.
|
|
21
|
+
Never reported on a guess, and never cached.
|
|
22
|
+
|
|
23
|
+
``AGENTOS_IS_VPS`` overrides the check: ``1`` for a VPS behind NAT whose public
|
|
24
|
+
address is not on any interface, ``0`` to silence a machine outright.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import ipaddress
|
|
29
|
+
import logging
|
|
30
|
+
import os
|
|
31
|
+
import socket
|
|
32
|
+
import time
|
|
33
|
+
from typing import TYPE_CHECKING, Any
|
|
34
|
+
|
|
35
|
+
if TYPE_CHECKING: # pragma: no cover — typing only
|
|
36
|
+
from sdk.client import BackendClient
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger("sdk.vps_identity")
|
|
39
|
+
|
|
40
|
+
#: A machine does not change into (or out of) a VPS often; a definite answer is
|
|
41
|
+
#: reused this long so a 5-minute shipper does not fetch the profile every tick.
|
|
42
|
+
CACHE_TTL_S = 6 * 3600.0
|
|
43
|
+
|
|
44
|
+
_TRUTHY = {"1", "true", "yes", "on"}
|
|
45
|
+
_FALSEY = {"0", "false", "no", "off"}
|
|
46
|
+
|
|
47
|
+
# Profile shapes the ML backend has used for the VPS address — the same tolerant
|
|
48
|
+
# set the AI backend reads (devops-bot-ai shared/dependencies._extract_vps_ip).
|
|
49
|
+
_IP_KEYS = ("vpsIp", "vps_ip", "vpsIP", "vpsIpAddress", "vpsAddress", "vpsHost",
|
|
50
|
+
"serverIp", "server_ip", "ipAddress", "ip_address", "ip")
|
|
51
|
+
_NESTED_KEYS = ("vps", "server", "machine", "allocation")
|
|
52
|
+
_NESTED_FIELDS = ("ipAddress", "ip_address", "ip", "host", "address")
|
|
53
|
+
|
|
54
|
+
_cache: dict[str, Any] = {"at": 0.0, "value": None}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _setting(name: str) -> str:
|
|
58
|
+
"""Process env first, then ``~/.agentos/.env`` — same resolution as the panel toggles."""
|
|
59
|
+
val = (os.environ.get(name) or "").strip()
|
|
60
|
+
if val:
|
|
61
|
+
return val
|
|
62
|
+
try:
|
|
63
|
+
from sdk.config import _dotenv_value
|
|
64
|
+
return (_dotenv_value(name) or "").strip()
|
|
65
|
+
except Exception: # noqa: BLE001 — a config read must never break reporting
|
|
66
|
+
return ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def override() -> bool | None:
|
|
70
|
+
"""``AGENTOS_IS_VPS`` as a bool, or ``None`` when unset/unrecognised."""
|
|
71
|
+
raw = _setting("AGENTOS_IS_VPS").lower()
|
|
72
|
+
if raw in _TRUTHY:
|
|
73
|
+
return True
|
|
74
|
+
if raw in _FALSEY:
|
|
75
|
+
return False
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _as_ip(value: str) -> str:
|
|
80
|
+
"""The bare IP in ``value`` (which may be a URL or ``host:port``), or ``""``."""
|
|
81
|
+
v = value.strip()
|
|
82
|
+
if "://" in v:
|
|
83
|
+
v = v.split("://", 1)[1]
|
|
84
|
+
v = v.split("/", 1)[0]
|
|
85
|
+
if v.startswith("[") and "]" in v: # [ipv6]:port
|
|
86
|
+
v = v[1:v.index("]")]
|
|
87
|
+
elif v.count(":") == 1: # ipv4:port / host:port
|
|
88
|
+
v = v.split(":", 1)[0]
|
|
89
|
+
try:
|
|
90
|
+
return str(ipaddress.ip_address(v))
|
|
91
|
+
except ValueError:
|
|
92
|
+
return ""
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def extract_vps_ip(user: dict) -> str:
|
|
96
|
+
"""The assigned VPS IP from a profile ``user`` object; ``""`` when it names none."""
|
|
97
|
+
for k in _IP_KEYS:
|
|
98
|
+
v = user.get(k)
|
|
99
|
+
if isinstance(v, str) and _as_ip(v):
|
|
100
|
+
return _as_ip(v)
|
|
101
|
+
for k in _NESTED_KEYS:
|
|
102
|
+
obj = user.get(k)
|
|
103
|
+
if isinstance(obj, str) and _as_ip(obj):
|
|
104
|
+
return _as_ip(obj)
|
|
105
|
+
if isinstance(obj, dict):
|
|
106
|
+
for f in _NESTED_FIELDS:
|
|
107
|
+
v = obj.get(f)
|
|
108
|
+
if isinstance(v, str) and _as_ip(v):
|
|
109
|
+
return _as_ip(v)
|
|
110
|
+
return ""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def local_addresses() -> set[str]:
|
|
114
|
+
"""Every address this machine holds: interfaces, hostname, route to the internet."""
|
|
115
|
+
found: set[str] = set()
|
|
116
|
+
try:
|
|
117
|
+
import psutil
|
|
118
|
+
for addrs in psutil.net_if_addrs().values():
|
|
119
|
+
for a in addrs:
|
|
120
|
+
if a.family in (socket.AF_INET, socket.AF_INET6):
|
|
121
|
+
ip = _as_ip(a.address.split("%", 1)[0])
|
|
122
|
+
if ip:
|
|
123
|
+
found.add(ip)
|
|
124
|
+
except Exception: # noqa: BLE001 — psutil is an optional extra
|
|
125
|
+
pass
|
|
126
|
+
try:
|
|
127
|
+
for info in socket.getaddrinfo(socket.gethostname(), None):
|
|
128
|
+
ip = _as_ip(str(info[4][0]).split("%", 1)[0])
|
|
129
|
+
if ip:
|
|
130
|
+
found.add(ip)
|
|
131
|
+
except Exception: # noqa: BLE001
|
|
132
|
+
pass
|
|
133
|
+
try:
|
|
134
|
+
from sdk.vps_health import _outbound_ip
|
|
135
|
+
ip = _as_ip(_outbound_ip())
|
|
136
|
+
if ip:
|
|
137
|
+
found.add(ip)
|
|
138
|
+
except Exception: # noqa: BLE001
|
|
139
|
+
pass
|
|
140
|
+
return found
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def is_assigned_vps(client: "BackendClient | None" = None) -> bool | None:
|
|
144
|
+
"""``True`` / ``False`` / ``None`` (unknown) — see the module docstring. Never raises."""
|
|
145
|
+
forced = override()
|
|
146
|
+
if forced is not None:
|
|
147
|
+
return forced
|
|
148
|
+
now = time.monotonic()
|
|
149
|
+
if _cache["value"] is not None and now - _cache["at"] < CACHE_TTL_S:
|
|
150
|
+
return _cache["value"]
|
|
151
|
+
try:
|
|
152
|
+
if client is None:
|
|
153
|
+
from sdk.client import BackendClient
|
|
154
|
+
client = BackendClient.from_config()
|
|
155
|
+
assigned = await client.assigned_vps_ip()
|
|
156
|
+
except Exception as exc: # noqa: BLE001 — offline / auth / not configured
|
|
157
|
+
logger.info("vps_identity.unknown err=%s", exc)
|
|
158
|
+
return None
|
|
159
|
+
if not assigned:
|
|
160
|
+
value, why = False, "no VPS is assigned to this user"
|
|
161
|
+
else:
|
|
162
|
+
value = assigned in local_addresses()
|
|
163
|
+
why = (f"this machine holds {assigned}" if value else
|
|
164
|
+
f"this machine does not hold the assigned VPS address {assigned} — "
|
|
165
|
+
f"set AGENTOS_IS_VPS=1 if it is the VPS behind NAT")
|
|
166
|
+
if value != _cache["value"]:
|
|
167
|
+
logger.info("vps_identity.%s (%s)", "assigned_vps" if value else "not_assigned_vps", why)
|
|
168
|
+
_cache.update(at=now, value=value)
|
|
169
|
+
return value
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def reset_cache() -> None:
|
|
173
|
+
"""Forget the cached answer (tests, and after the user's VPS changes)."""
|
|
174
|
+
_cache.update(at=0.0, value=None)
|
|
@@ -139,6 +139,15 @@ class _FakeClient:
|
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
class TestReportOnce:
|
|
142
|
+
@pytest.fixture(autouse=True)
|
|
143
|
+
def on_the_assigned_vps(self, monkeypatch):
|
|
144
|
+
"""These tests are about the PATCH; the VPS-only gate has its own tests."""
|
|
145
|
+
from sdk import vps_identity
|
|
146
|
+
|
|
147
|
+
async def yes(client=None):
|
|
148
|
+
return True
|
|
149
|
+
monkeypatch.setattr(vps_identity, "is_assigned_vps", yes)
|
|
150
|
+
|
|
142
151
|
def test_patches_and_records_last(self, monkeypatch):
|
|
143
152
|
monkeypatch.setattr(vps_health, "build_payload", lambda: {"status": "RUNNING",
|
|
144
153
|
"lastPingedAt": "t"})
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Only the user's assigned VPS reports health and ships activity.
|
|
2
|
+
|
|
3
|
+
The incident: a user ran the SDK on a laptop AND on their VPS with the same
|
|
4
|
+
``co_`` token. Both health pings and the activity relay are keyed server-side on
|
|
5
|
+
the token, so the laptop's CPU/RAM/disk/uptime overwrote the VPS row every two
|
|
6
|
+
hours (the "VPS" booted when the laptop did, and showed the laptop's 85% disk),
|
|
7
|
+
and the laptop's events were filed under the VPS's audit trail.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from sdk import vps_health, vps_identity
|
|
16
|
+
from sdk.activity import shipper
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(autouse=True)
|
|
20
|
+
def hermetic(monkeypatch):
|
|
21
|
+
import sdk.config as cfg
|
|
22
|
+
monkeypatch.setattr(cfg, "_dotenv_value", lambda key: "")
|
|
23
|
+
monkeypatch.delenv("AGENTOS_IS_VPS", raising=False)
|
|
24
|
+
vps_identity.reset_cache()
|
|
25
|
+
yield
|
|
26
|
+
vps_identity.reset_cache()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _Client:
|
|
30
|
+
def __init__(self, assigned: str | Exception = "77.68.23.23", ok: bool = True) -> None:
|
|
31
|
+
self.assigned = assigned
|
|
32
|
+
self.ok = ok
|
|
33
|
+
self.profile_calls = 0
|
|
34
|
+
self.reports: list[dict] = []
|
|
35
|
+
|
|
36
|
+
async def assigned_vps_ip(self) -> str:
|
|
37
|
+
self.profile_calls += 1
|
|
38
|
+
if isinstance(self.assigned, Exception):
|
|
39
|
+
raise self.assigned
|
|
40
|
+
return self.assigned
|
|
41
|
+
|
|
42
|
+
async def report_vps_allocation(self, payload: dict) -> bool:
|
|
43
|
+
self.reports.append(payload)
|
|
44
|
+
return self.ok
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _holds(monkeypatch, *ips: str) -> None:
|
|
48
|
+
monkeypatch.setattr(vps_identity, "local_addresses", lambda: set(ips))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class TestProfileParsing:
|
|
52
|
+
@pytest.mark.parametrize("user, want", [
|
|
53
|
+
({"vps": {"ipAddress": "77.68.23.23"}}, "77.68.23.23"),
|
|
54
|
+
({"vpsIp": "77.68.23.23"}, "77.68.23.23"),
|
|
55
|
+
({"vps": {"url": "http://77.68.23.23:8765"}, "vpsHost": "http://77.68.23.23:8765"},
|
|
56
|
+
"77.68.23.23"),
|
|
57
|
+
({"server": {"host": "77.68.23.23:22"}}, "77.68.23.23"),
|
|
58
|
+
({"vps": None}, ""),
|
|
59
|
+
({"vps": {"ipAddress": "not-an-ip"}}, ""),
|
|
60
|
+
({}, ""),
|
|
61
|
+
])
|
|
62
|
+
def test_extracts_the_assigned_ip(self, user, want):
|
|
63
|
+
assert vps_identity.extract_vps_ip(user) == want
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestIsAssignedVps:
|
|
67
|
+
def test_the_vps_itself(self, monkeypatch):
|
|
68
|
+
_holds(monkeypatch, "127.0.0.1", "77.68.23.23")
|
|
69
|
+
assert asyncio.run(vps_identity.is_assigned_vps(_Client())) is True
|
|
70
|
+
|
|
71
|
+
def test_a_laptop_with_the_same_token(self, monkeypatch):
|
|
72
|
+
_holds(monkeypatch, "127.0.0.1", "192.168.18.16")
|
|
73
|
+
assert asyncio.run(vps_identity.is_assigned_vps(_Client())) is False
|
|
74
|
+
|
|
75
|
+
def test_a_user_with_no_vps(self, monkeypatch):
|
|
76
|
+
_holds(monkeypatch, "77.68.23.23")
|
|
77
|
+
assert asyncio.run(vps_identity.is_assigned_vps(_Client(assigned=""))) is False
|
|
78
|
+
|
|
79
|
+
def test_profile_unreachable_is_unknown_and_not_cached(self, monkeypatch):
|
|
80
|
+
_holds(monkeypatch, "77.68.23.23")
|
|
81
|
+
down = _Client(assigned=RuntimeError("offline"))
|
|
82
|
+
assert asyncio.run(vps_identity.is_assigned_vps(down)) is None
|
|
83
|
+
up = _Client()
|
|
84
|
+
assert asyncio.run(vps_identity.is_assigned_vps(up)) is True
|
|
85
|
+
assert up.profile_calls == 1
|
|
86
|
+
|
|
87
|
+
def test_a_definite_answer_is_cached(self, monkeypatch):
|
|
88
|
+
_holds(monkeypatch, "77.68.23.23")
|
|
89
|
+
client = _Client()
|
|
90
|
+
for _ in range(3):
|
|
91
|
+
assert asyncio.run(vps_identity.is_assigned_vps(client)) is True
|
|
92
|
+
assert client.profile_calls == 1
|
|
93
|
+
|
|
94
|
+
@pytest.mark.parametrize("raw, want", [("1", True), ("true", True), ("0", False), ("off", False)])
|
|
95
|
+
def test_override_wins_without_a_profile_call(self, monkeypatch, raw, want):
|
|
96
|
+
monkeypatch.setenv("AGENTOS_IS_VPS", raw)
|
|
97
|
+
_holds(monkeypatch) # holds nothing
|
|
98
|
+
client = _Client()
|
|
99
|
+
assert asyncio.run(vps_identity.is_assigned_vps(client)) is want
|
|
100
|
+
assert client.profile_calls == 0
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class TestHealthOnlyFromTheVps:
|
|
104
|
+
def test_a_laptop_sends_no_ping(self, monkeypatch):
|
|
105
|
+
_holds(monkeypatch, "192.168.18.16")
|
|
106
|
+
monkeypatch.setattr(vps_health, "build_payload", lambda: {"status": "RUNNING"})
|
|
107
|
+
client = _Client()
|
|
108
|
+
assert asyncio.run(vps_health.report_once(client)) is False
|
|
109
|
+
assert client.reports == []
|
|
110
|
+
assert vps_health.last_report()["last"]["reason"] == "not_assigned_vps"
|
|
111
|
+
|
|
112
|
+
def test_unknown_sends_no_ping(self, monkeypatch):
|
|
113
|
+
monkeypatch.setattr(vps_health, "build_payload", lambda: {"status": "RUNNING"})
|
|
114
|
+
client = _Client(assigned=RuntimeError("offline"))
|
|
115
|
+
assert asyncio.run(vps_health.report_once(client)) is False
|
|
116
|
+
assert client.reports == []
|
|
117
|
+
assert vps_health.last_report()["last"]["reason"] == "vps_identity_unknown"
|
|
118
|
+
|
|
119
|
+
def test_the_vps_pings(self, monkeypatch):
|
|
120
|
+
_holds(monkeypatch, "77.68.23.23")
|
|
121
|
+
monkeypatch.setattr(vps_health, "build_payload", lambda: {"status": "RUNNING"})
|
|
122
|
+
client = _Client()
|
|
123
|
+
assert asyncio.run(vps_health.report_once(client)) is True
|
|
124
|
+
assert client.reports == [{"status": "RUNNING"}]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class TestActivityOnlyFromTheVps:
|
|
128
|
+
@pytest.fixture
|
|
129
|
+
def flushed(self, monkeypatch):
|
|
130
|
+
calls = []
|
|
131
|
+
|
|
132
|
+
async def fake_flush():
|
|
133
|
+
calls.append(1)
|
|
134
|
+
return shipper.FlushResult()
|
|
135
|
+
monkeypatch.setattr(shipper, "flush_once", fake_flush)
|
|
136
|
+
return calls
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _answer(value):
|
|
140
|
+
async def f():
|
|
141
|
+
return value
|
|
142
|
+
return f
|
|
143
|
+
|
|
144
|
+
def test_the_vps_ships(self, flushed):
|
|
145
|
+
asyncio.run(shipper.ship_tick(self._answer(True)))
|
|
146
|
+
assert flushed == [1]
|
|
147
|
+
|
|
148
|
+
def test_unknown_keeps_everything_for_next_tick(self, monkeypatch, flushed):
|
|
149
|
+
discarded = []
|
|
150
|
+
monkeypatch.setattr(shipper, "discard_spool", lambda spool=None: discarded.append(1) or 0)
|
|
151
|
+
asyncio.run(shipper.ship_tick(self._answer(None)))
|
|
152
|
+
assert flushed == [] and discarded == []
|
|
153
|
+
|
|
154
|
+
def test_a_laptop_discards_instead_of_shipping(self, monkeypatch, flushed):
|
|
155
|
+
discarded = []
|
|
156
|
+
monkeypatch.setattr(shipper, "discard_spool", lambda spool=None: discarded.append(1) or 7)
|
|
157
|
+
asyncio.run(shipper.ship_tick(self._answer(False)))
|
|
158
|
+
assert flushed == [] and discarded == [1]
|
|
159
|
+
|
|
160
|
+
def test_discard_empties_a_real_spool(self, tmp_path):
|
|
161
|
+
from sdk.activity.spool import Spool
|
|
162
|
+
|
|
163
|
+
spool = Spool(tmp_path / "activity.db")
|
|
164
|
+
for i in range(3):
|
|
165
|
+
spool.add_event({"eventId": f"e{i}", "occurredAt": "2026-10-22T00:00:00Z",
|
|
166
|
+
"category": "COMMAND", "action": "exec"})
|
|
167
|
+
assert spool.count() == 3
|
|
168
|
+
assert shipper.discard_spool(spool) == 3
|
|
169
|
+
assert spool.count() == 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestActivityFitsTheGatewayLimit:
|
|
173
|
+
"""The gateway 400s an `activity` over 2000 chars; the cap was 8000."""
|
|
174
|
+
|
|
175
|
+
def test_short_text_is_untouched(self):
|
|
176
|
+
from sdk.client import _fit_activity
|
|
177
|
+
assert _fit_activity("ran 3 checks") == "ran 3 checks"
|
|
178
|
+
|
|
179
|
+
def test_long_text_keeps_the_tail_within_the_limit(self):
|
|
180
|
+
from sdk.client import _ACTIVITY_MAX_CHARS, _fit_activity
|
|
181
|
+
text = "old line\n" * 500 + "FINAL SUMMARY"
|
|
182
|
+
out = _fit_activity(text)
|
|
183
|
+
assert len(out) <= _ACTIVITY_MAX_CHARS == 2000
|
|
184
|
+
assert out.startswith("…\n") and out.endswith("FINAL SUMMARY")
|
|
185
|
+
|
|
186
|
+
def test_agent_activity_post_is_trimmed(self, monkeypatch):
|
|
187
|
+
import httpx
|
|
188
|
+
from sdk.client import BackendClient
|
|
189
|
+
|
|
190
|
+
sent = {}
|
|
191
|
+
|
|
192
|
+
class _Resp:
|
|
193
|
+
status_code = 201
|
|
194
|
+
text = ""
|
|
195
|
+
|
|
196
|
+
class _HTTP:
|
|
197
|
+
def __init__(self, *a, **k): pass
|
|
198
|
+
async def __aenter__(self): return self
|
|
199
|
+
async def __aexit__(self, *a): return False
|
|
200
|
+
async def post(self, url, json=None, headers=None):
|
|
201
|
+
sent.update(json)
|
|
202
|
+
return _Resp()
|
|
203
|
+
monkeypatch.setattr(httpx, "AsyncClient", _HTTP)
|
|
204
|
+
client = BackendClient.__new__(BackendClient)
|
|
205
|
+
client._headers = {}
|
|
206
|
+
assert asyncio.run(client.log_agent_activity("x" * 5000, agent_id="a1")) is True
|
|
207
|
+
assert len(sent["activity"]) == 2000
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestAuditFlagIsTheUsersNotRoots:
|
|
211
|
+
"""Under sudo, a flag in /root/.agentos/.env answered for the panel user."""
|
|
212
|
+
|
|
213
|
+
def test_roots_dotenv_does_not_answer_for_the_panel_user(self, monkeypatch, tmp_path):
|
|
214
|
+
from sdk.activity import os_setup
|
|
215
|
+
|
|
216
|
+
user_home = tmp_path / "nouman"
|
|
217
|
+
monkeypatch.delenv(os_setup.AUDIT_FLAG, raising=False)
|
|
218
|
+
monkeypatch.setattr(os_setup, "_is_privileged", lambda: True)
|
|
219
|
+
monkeypatch.setattr(os_setup, "_panel_user", lambda: ("nouman", user_home, 1000, 1000))
|
|
220
|
+
monkeypatch.setattr(os_setup.settings, "setting", lambda name: "1") # root's .env
|
|
221
|
+
assert os_setup._flag_value() == ""
|
|
222
|
+
|
|
223
|
+
(user_home / ".agentos").mkdir(parents=True)
|
|
224
|
+
(user_home / ".agentos" / ".env").write_text(f"{os_setup.AUDIT_FLAG}=1\n")
|
|
225
|
+
assert os_setup._flag_value() == "1"
|
|
226
|
+
|
|
227
|
+
def test_unprivileged_still_reads_settings(self, monkeypatch, tmp_path):
|
|
228
|
+
from sdk.activity import os_setup
|
|
229
|
+
|
|
230
|
+
monkeypatch.delenv(os_setup.AUDIT_FLAG, raising=False)
|
|
231
|
+
monkeypatch.setattr(os_setup, "_is_privileged", lambda: False)
|
|
232
|
+
monkeypatch.setattr(os_setup.Path, "home", classmethod(lambda cls: tmp_path))
|
|
233
|
+
monkeypatch.setattr(os_setup.settings, "setting", lambda name: "1")
|
|
234
|
+
assert os_setup._flag_value() == "1"
|
|
File without changes
|
{devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|