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.
Files changed (235) hide show
  1. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
  4. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/os_setup.py +5 -0
  6. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/shipper.py +46 -1
  7. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/activity.py +1 -1
  8. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/client.py +39 -8
  9. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/vps_health.py +9 -1
  10. devops_bot_sdk-1.6.70/sdk/vps_identity.py +174 -0
  11. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_vps_health.py +9 -0
  12. devops_bot_sdk-1.6.70/tests/test_vps_only_reporting.py +234 -0
  13. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/README.md +0 -0
  14. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  15. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  16. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/requires.txt +0 -0
  17. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  18. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/pyproject.toml +0 -0
  19. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/__init__.py +0 -0
  20. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/classifier.py +0 -0
  21. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/__init__.py +0 -0
  22. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/common.py +0 -0
  23. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/linux_auditd.py +0 -0
  24. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/macos_eslogger.py +0 -0
  25. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/collectors/windows_eventlog.py +0 -0
  26. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/models.py +0 -0
  27. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/recorder.py +0 -0
  28. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/redact.py +0 -0
  29. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/settings.py +0 -0
  30. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/activity/spool.py +0 -0
  31. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agentd.py +0 -0
  32. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/__init__.py +0 -0
  33. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/builder.py +0 -0
  34. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/chat.py +0 -0
  35. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/connectors.py +0 -0
  36. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/__init__.py +0 -0
  37. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/claude_code.py +0 -0
  38. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/http.py +0 -0
  39. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/mcp.py +0 -0
  40. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/n8n.py +0 -0
  41. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/engines/shell.py +0 -0
  42. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/integrations.py +0 -0
  43. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/inventory.py +0 -0
  44. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/library/__init__.py +0 -0
  45. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/manifest.py +0 -0
  46. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_studio.py +0 -0
  47. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_template.py +0 -0
  48. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/prompt_vars.py +0 -0
  49. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/run_log.py +0 -0
  50. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/schedules.py +0 -0
  51. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/secrets.py +0 -0
  52. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/ticket_run.py +0 -0
  53. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/usage.py +0 -0
  54. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/agents/varstore.py +0 -0
  55. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/browser_profile.py +0 -0
  56. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/cli.py +0 -0
  57. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/collectors/__init__.py +0 -0
  58. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/collectors/files.py +0 -0
  59. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/collectors/process.py +0 -0
  60. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/collectors/screenshot.py +0 -0
  61. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/config.py +0 -0
  62. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/crucial.py +0 -0
  63. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/custom_agents.py +0 -0
  64. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/departments.py +0 -0
  65. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/__init__.py +0 -0
  66. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/assets.py +0 -0
  67. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/bootstrap.py +0 -0
  68. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/browser.py +0 -0
  69. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/compare.py +0 -0
  70. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/design_verify/loop.py +0 -0
  71. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/estimator.py +0 -0
  72. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/exceptions.py +0 -0
  73. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/firewall.py +0 -0
  74. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/formats.py +0 -0
  75. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/git_ops.py +0 -0
  76. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/github_fetch.py +0 -0
  77. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/github_ingest.py +0 -0
  78. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/graph_recon.py +0 -0
  79. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/graphify.py +0 -0
  80. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/hooks/__init__.py +0 -0
  81. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/hooks/crucial_guard.py +0 -0
  82. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/intake.py +0 -0
  83. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ipc/__init__.py +0 -0
  84. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ipc/electron_bridge.py +0 -0
  85. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ipc/handlers.py +0 -0
  86. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/journal.py +0 -0
  87. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/local_exec.py +0 -0
  88. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/memory_guard.py +0 -0
  89. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/models/__init__.py +0 -0
  90. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/models/envelope.py +0 -0
  91. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/models/requests.py +0 -0
  92. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/models/responses.py +0 -0
  93. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/models/snapshots.py +0 -0
  94. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/phase_timer.py +0 -0
  95. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/plan_run.py +0 -0
  96. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/platform_compat.py +0 -0
  97. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/prd_progress.py +0 -0
  98. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/proof_readers.py +0 -0
  99. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/providers.py +0 -0
  100. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/py.typed +0 -0
  101. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/resume.py +0 -0
  102. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/router.py +0 -0
  103. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/run_auto.py +0 -0
  104. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/schedule_tickets.py +0 -0
  105. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/scheduler.py +0 -0
  106. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/self_report.py +0 -0
  107. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/self_report_local.py +0 -0
  108. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/sse.py +0 -0
  109. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/swagger.py +0 -0
  110. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/task_file.py +0 -0
  111. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/task_types.py +0 -0
  112. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/test.py +0 -0
  113. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/test_pipeline.py +0 -0
  114. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ticket_fields.py +0 -0
  115. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/tickets.py +0 -0
  116. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/tiers/__init__.py +0 -0
  117. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/tiers/mini_agent.py +0 -0
  118. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/__init__.py +0 -0
  119. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/api.py +0 -0
  120. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/dashboard.py +0 -0
  121. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/health.py +0 -0
  122. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/ip_allowlist.py +0 -0
  123. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/jobs.py +0 -0
  124. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/models.py +0 -0
  125. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/runner.py +0 -0
  126. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/security.py +0 -0
  127. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/server.py +0 -0
  128. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/service.py +0 -0
  129. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/session.py +0 -0
  130. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/setup_manager.py +0 -0
  131. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/agents.html +0 -0
  132. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/app.js +0 -0
  133. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/chat.html +0 -0
  134. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/custom-agents.html +0 -0
  135. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/default-agents.html +0 -0
  136. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/index.html +0 -0
  137. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/report.html +0 -0
  138. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/runs.html +0 -0
  139. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/schedules.html +0 -0
  140. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/setup.html +0 -0
  141. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/styles.css +0 -0
  142. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/static/tickets.html +0 -0
  143. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/ui/swagger.py +0 -0
  144. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/updater.py +0 -0
  145. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/url_access.py +0 -0
  146. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
  147. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/sdk/write_hook.py +0 -0
  148. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/setup.cfg +0 -0
  149. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_activity_collectors.py +0 -0
  150. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_activity_core.py +0 -0
  151. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_activity_macos.py +0 -0
  152. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_activity_os_setup.py +0 -0
  153. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_activity_pipeline.py +0 -0
  154. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_activity.py +0 -0
  155. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_activity_tracking.py +0 -0
  156. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_chat_features.py +0 -0
  157. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_crud_cli.py +0 -0
  158. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_listing.py +0 -0
  159. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_prompt_async.py +0 -0
  160. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_prompt_models.py +0 -0
  161. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_run_ticket.py +0 -0
  162. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_schedules.py +0 -0
  163. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agent_workspace_settings.py +0 -0
  164. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_agents_connectors_builder.py +0 -0
  165. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_allowed_tools_config.py +0 -0
  166. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_auto_agent_dispatch.py +0 -0
  167. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_browser_profile.py +0 -0
  168. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_cheap_tier.py +0 -0
  169. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_client_jira_projects.py +0 -0
  170. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_config_token_diagnostics.py +0 -0
  171. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_configure_token_owner.py +0 -0
  172. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_console_encoding.py +0 -0
  173. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_crash_recovery.py +0 -0
  174. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_departments.py +0 -0
  175. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_done_transition.py +0 -0
  176. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_epic_decompose_run.py +0 -0
  177. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_epic_inherited_agent_id.py +0 -0
  178. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_epic_session_isolation.py +0 -0
  179. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_estimator.py +0 -0
  180. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_fallback_cli.py +0 -0
  181. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_firewall_open_on_update.py +0 -0
  182. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_formats.py +0 -0
  183. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_git_action.py +0 -0
  184. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_github_fetch.py +0 -0
  185. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_github_ingest.py +0 -0
  186. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_graph_recon.py +0 -0
  187. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_graphify_build.py +0 -0
  188. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_graphify_hook_install.py +0 -0
  189. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_health.py +0 -0
  190. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_intake_job_outcome.py +0 -0
  191. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_intake_plan_review.py +0 -0
  192. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_journal.py +0 -0
  193. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_local_exec_provider.py +0 -0
  194. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_mcp_route_contract.py +0 -0
  195. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_memory_guard.py +0 -0
  196. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_phase_timer.py +0 -0
  197. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_plan_new_command.py +0 -0
  198. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_plan_run_activity.py +0 -0
  199. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_pr_identity.py +0 -0
  200. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_prd_artifact.py +0 -0
  201. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_project_path_resolution.py +0 -0
  202. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_proof_readers.py +0 -0
  203. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_providers.py +0 -0
  204. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_recon_session_isolation.py +0 -0
  205. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_resume.py +0 -0
  206. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_router.py +0 -0
  207. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_run_auto_agent_only.py +0 -0
  208. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_run_outputs.py +0 -0
  209. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_run_verdict_is_honest.py +0 -0
  210. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_schedule_api.py +0 -0
  211. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_schedule_outputs_and_plans.py +0 -0
  212. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_schedule_tickets.py +0 -0
  213. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_scheduler.py +0 -0
  214. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_scheduler_transient_retry.py +0 -0
  215. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_self_report.py +0 -0
  216. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_service_token_warning.py +0 -0
  217. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_stale_panel_restart.py +0 -0
  218. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_swagger_command.py +0 -0
  219. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_task_file_and_plan_run.py +0 -0
  220. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_task_file_format_doc.py +0 -0
  221. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ticket_cli.py +0 -0
  222. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ticket_fields.py +0 -0
  223. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ticket_run_resolve_by_id.py +0 -0
  224. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ticket_speed_fixes.py +0 -0
  225. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_tier_flag_parity.py +0 -0
  226. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ui_gate.py +0 -0
  227. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_ui_lifespan.py +0 -0
  228. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_update_timer.py +0 -0
  229. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_updater_extras.py +0 -0
  230. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_updater_pep668.py +0 -0
  231. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_updater_reports_the_truth.py +0 -0
  232. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_url_access.py +0 -0
  233. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_windows_loop_identity.py +0 -0
  234. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_windows_task_paths.py +0 -0
  235. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.70}/tests/test_write_hook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.69
3
+ Version: 1.6.70
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.69
3
+ Version: 1.6.70
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -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.69
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.69" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
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 flush_once()
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 # bounded like _ACTIVITY_FLUSH_MAX_CHARS
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
- # Cap the coalesced payload so one flush can't POST a multi-megabyte body after a
125
- # very long, chatty run; keep the tail (most recent lines are the most useful).
126
- _ACTIVITY_FLUSH_MAX_CHARS = 8000
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"