devops-bot-sdk 1.6.69__tar.gz → 1.6.71__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 (236) hide show
  1. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/SOURCES.txt +3 -0
  4. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/os_setup.py +5 -0
  6. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/settings.py +21 -1
  7. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/shipper.py +75 -3
  8. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/spool.py +51 -16
  9. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/activity.py +1 -1
  10. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/client.py +39 -8
  11. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/vps_health.py +9 -1
  12. devops_bot_sdk-1.6.71/sdk/vps_identity.py +174 -0
  13. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_activity_collectors.py +8 -1
  14. devops_bot_sdk-1.6.71/tests/test_activity_live_tracking.py +145 -0
  15. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_activity_macos.py +8 -1
  16. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_activity_pipeline.py +18 -14
  17. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_vps_health.py +9 -0
  18. devops_bot_sdk-1.6.71/tests/test_vps_only_reporting.py +234 -0
  19. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/README.md +0 -0
  20. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  21. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  22. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/requires.txt +0 -0
  23. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  24. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/pyproject.toml +0 -0
  25. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/__init__.py +0 -0
  26. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/classifier.py +0 -0
  27. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/__init__.py +0 -0
  28. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/common.py +0 -0
  29. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/linux_auditd.py +0 -0
  30. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/macos_eslogger.py +0 -0
  31. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/collectors/windows_eventlog.py +0 -0
  32. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/models.py +0 -0
  33. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/recorder.py +0 -0
  34. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/activity/redact.py +0 -0
  35. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agentd.py +0 -0
  36. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/__init__.py +0 -0
  37. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/builder.py +0 -0
  38. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/chat.py +0 -0
  39. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/connectors.py +0 -0
  40. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/__init__.py +0 -0
  41. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/claude_code.py +0 -0
  42. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/http.py +0 -0
  43. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/mcp.py +0 -0
  44. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/n8n.py +0 -0
  45. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/engines/shell.py +0 -0
  46. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/integrations.py +0 -0
  47. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/inventory.py +0 -0
  48. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/library/__init__.py +0 -0
  49. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/manifest.py +0 -0
  50. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/prompt_studio.py +0 -0
  51. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/prompt_template.py +0 -0
  52. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/prompt_vars.py +0 -0
  53. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/run_log.py +0 -0
  54. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/schedules.py +0 -0
  55. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/secrets.py +0 -0
  56. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/ticket_run.py +0 -0
  57. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/usage.py +0 -0
  58. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/agents/varstore.py +0 -0
  59. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/browser_profile.py +0 -0
  60. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/cli.py +0 -0
  61. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/collectors/__init__.py +0 -0
  62. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/collectors/files.py +0 -0
  63. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/collectors/process.py +0 -0
  64. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/collectors/screenshot.py +0 -0
  65. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/config.py +0 -0
  66. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/crucial.py +0 -0
  67. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/custom_agents.py +0 -0
  68. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/departments.py +0 -0
  69. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/__init__.py +0 -0
  70. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/assets.py +0 -0
  71. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/bootstrap.py +0 -0
  72. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/browser.py +0 -0
  73. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/compare.py +0 -0
  74. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/design_verify/loop.py +0 -0
  75. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/estimator.py +0 -0
  76. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/exceptions.py +0 -0
  77. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/firewall.py +0 -0
  78. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/formats.py +0 -0
  79. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/git_ops.py +0 -0
  80. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/github_fetch.py +0 -0
  81. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/github_ingest.py +0 -0
  82. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/graph_recon.py +0 -0
  83. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/graphify.py +0 -0
  84. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/hooks/__init__.py +0 -0
  85. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/hooks/crucial_guard.py +0 -0
  86. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/intake.py +0 -0
  87. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ipc/__init__.py +0 -0
  88. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ipc/electron_bridge.py +0 -0
  89. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ipc/handlers.py +0 -0
  90. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/journal.py +0 -0
  91. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/local_exec.py +0 -0
  92. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/memory_guard.py +0 -0
  93. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/models/__init__.py +0 -0
  94. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/models/envelope.py +0 -0
  95. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/models/requests.py +0 -0
  96. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/models/responses.py +0 -0
  97. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/models/snapshots.py +0 -0
  98. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/phase_timer.py +0 -0
  99. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/plan_run.py +0 -0
  100. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/platform_compat.py +0 -0
  101. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/prd_progress.py +0 -0
  102. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/proof_readers.py +0 -0
  103. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/providers.py +0 -0
  104. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/py.typed +0 -0
  105. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/resume.py +0 -0
  106. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/router.py +0 -0
  107. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/run_auto.py +0 -0
  108. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/schedule_tickets.py +0 -0
  109. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/scheduler.py +0 -0
  110. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/self_report.py +0 -0
  111. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/self_report_local.py +0 -0
  112. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/sse.py +0 -0
  113. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/swagger.py +0 -0
  114. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/task_file.py +0 -0
  115. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/task_types.py +0 -0
  116. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/test.py +0 -0
  117. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/test_pipeline.py +0 -0
  118. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ticket_fields.py +0 -0
  119. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/tickets.py +0 -0
  120. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/tiers/__init__.py +0 -0
  121. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/tiers/mini_agent.py +0 -0
  122. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/__init__.py +0 -0
  123. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/api.py +0 -0
  124. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/dashboard.py +0 -0
  125. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/health.py +0 -0
  126. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/ip_allowlist.py +0 -0
  127. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/jobs.py +0 -0
  128. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/models.py +0 -0
  129. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/runner.py +0 -0
  130. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/security.py +0 -0
  131. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/server.py +0 -0
  132. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/service.py +0 -0
  133. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/session.py +0 -0
  134. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/setup_manager.py +0 -0
  135. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/agents.html +0 -0
  136. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/app.js +0 -0
  137. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/chat.html +0 -0
  138. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/custom-agents.html +0 -0
  139. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/default-agents.html +0 -0
  140. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/index.html +0 -0
  141. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/report.html +0 -0
  142. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/runs.html +0 -0
  143. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/schedules.html +0 -0
  144. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/setup.html +0 -0
  145. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/styles.css +0 -0
  146. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/static/tickets.html +0 -0
  147. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/ui/swagger.py +0 -0
  148. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/updater.py +0 -0
  149. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/url_access.py +0 -0
  150. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
  151. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/sdk/write_hook.py +0 -0
  152. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/setup.cfg +0 -0
  153. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_activity_core.py +0 -0
  154. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_activity_os_setup.py +0 -0
  155. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_activity.py +0 -0
  156. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_activity_tracking.py +0 -0
  157. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_chat_features.py +0 -0
  158. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_crud_cli.py +0 -0
  159. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_listing.py +0 -0
  160. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_prompt_async.py +0 -0
  161. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_prompt_models.py +0 -0
  162. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_run_ticket.py +0 -0
  163. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_schedules.py +0 -0
  164. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agent_workspace_settings.py +0 -0
  165. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_agents_connectors_builder.py +0 -0
  166. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_allowed_tools_config.py +0 -0
  167. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_auto_agent_dispatch.py +0 -0
  168. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_browser_profile.py +0 -0
  169. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_cheap_tier.py +0 -0
  170. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_client_jira_projects.py +0 -0
  171. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_config_token_diagnostics.py +0 -0
  172. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_configure_token_owner.py +0 -0
  173. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_console_encoding.py +0 -0
  174. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_crash_recovery.py +0 -0
  175. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_departments.py +0 -0
  176. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_done_transition.py +0 -0
  177. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_epic_decompose_run.py +0 -0
  178. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_epic_inherited_agent_id.py +0 -0
  179. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_epic_session_isolation.py +0 -0
  180. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_estimator.py +0 -0
  181. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_fallback_cli.py +0 -0
  182. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_firewall_open_on_update.py +0 -0
  183. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_formats.py +0 -0
  184. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_git_action.py +0 -0
  185. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_github_fetch.py +0 -0
  186. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_github_ingest.py +0 -0
  187. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_graph_recon.py +0 -0
  188. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_graphify_build.py +0 -0
  189. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_graphify_hook_install.py +0 -0
  190. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_health.py +0 -0
  191. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_intake_job_outcome.py +0 -0
  192. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_intake_plan_review.py +0 -0
  193. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_journal.py +0 -0
  194. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_local_exec_provider.py +0 -0
  195. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_mcp_route_contract.py +0 -0
  196. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_memory_guard.py +0 -0
  197. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_phase_timer.py +0 -0
  198. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_plan_new_command.py +0 -0
  199. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_plan_run_activity.py +0 -0
  200. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_pr_identity.py +0 -0
  201. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_prd_artifact.py +0 -0
  202. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_project_path_resolution.py +0 -0
  203. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_proof_readers.py +0 -0
  204. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_providers.py +0 -0
  205. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_recon_session_isolation.py +0 -0
  206. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_resume.py +0 -0
  207. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_router.py +0 -0
  208. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_run_auto_agent_only.py +0 -0
  209. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_run_outputs.py +0 -0
  210. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_run_verdict_is_honest.py +0 -0
  211. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_schedule_api.py +0 -0
  212. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_schedule_outputs_and_plans.py +0 -0
  213. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_schedule_tickets.py +0 -0
  214. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_scheduler.py +0 -0
  215. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_scheduler_transient_retry.py +0 -0
  216. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_self_report.py +0 -0
  217. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_service_token_warning.py +0 -0
  218. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_stale_panel_restart.py +0 -0
  219. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_swagger_command.py +0 -0
  220. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_task_file_and_plan_run.py +0 -0
  221. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_task_file_format_doc.py +0 -0
  222. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ticket_cli.py +0 -0
  223. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ticket_fields.py +0 -0
  224. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ticket_run_resolve_by_id.py +0 -0
  225. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ticket_speed_fixes.py +0 -0
  226. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_tier_flag_parity.py +0 -0
  227. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ui_gate.py +0 -0
  228. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_ui_lifespan.py +0 -0
  229. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_update_timer.py +0 -0
  230. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_updater_extras.py +0 -0
  231. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_updater_pep668.py +0 -0
  232. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_updater_reports_the_truth.py +0 -0
  233. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_url_access.py +0 -0
  234. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_windows_loop_identity.py +0 -0
  235. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/tests/test_windows_task_paths.py +0 -0
  236. {devops_bot_sdk-1.6.69 → devops_bot_sdk-1.6.71}/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.71
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.71
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
@@ -143,6 +144,7 @@ sdk/ui/static/tickets.html
143
144
  sdk/vendor/aidraftsman-cli-0.1.0.tgz
144
145
  tests/test_activity_collectors.py
145
146
  tests/test_activity_core.py
147
+ tests/test_activity_live_tracking.py
146
148
  tests/test_activity_macos.py
147
149
  tests/test_activity_os_setup.py
148
150
  tests/test_activity_pipeline.py
@@ -226,6 +228,7 @@ tests/test_updater_pep668.py
226
228
  tests/test_updater_reports_the_truth.py
227
229
  tests/test_url_access.py
228
230
  tests/test_vps_health.py
231
+ tests/test_vps_only_reporting.py
229
232
  tests/test_windows_loop_identity.py
230
233
  tests/test_windows_task_paths.py
231
234
  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.71
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.71" # 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
 
@@ -23,8 +23,12 @@ from sdk.activity.spool import Spool
23
23
 
24
24
  logger = logging.getLogger("sdk.activity.settings")
25
25
 
26
- DEFAULT_SHIP_INTERVAL_S = 300.0
26
+ # Live tracking: one POST a minute is far under the relay's 60/min budget, and the
27
+ # five-minute default meant the newest activity was always up to five minutes stale.
28
+ DEFAULT_SHIP_INTERVAL_S = 60.0
27
29
  MIN_SHIP_INTERVAL_S = 30.0
30
+ #: Events older than this are discarded instead of shipped (``0`` keeps everything).
31
+ DEFAULT_MAX_AGE_S = 3600.0
28
32
  _FALSEY = {"0", "false", "no", "off"}
29
33
  _TRUTHY = {"1", "true", "yes", "on"}
30
34
 
@@ -63,6 +67,22 @@ def ship_interval_seconds() -> float:
63
67
  return DEFAULT_SHIP_INTERVAL_S
64
68
 
65
69
 
70
+ def max_age_seconds() -> float:
71
+ """``AGENTOS_ACTIVITY_MAX_AGE_S`` — how old an event may be and still ship.
72
+
73
+ Default one hour: the trail is for live tracking, and a backlog shipped
74
+ oldest-first kept busy hosts days behind. ``0`` disables the cut-off.
75
+ """
76
+ raw = setting("AGENTOS_ACTIVITY_MAX_AGE_S")
77
+ if not raw:
78
+ return DEFAULT_MAX_AGE_S
79
+ try:
80
+ return max(0.0, float(raw))
81
+ except ValueError:
82
+ logger.warning("AGENTOS_ACTIVITY_MAX_AGE_S=%r is not a number; using default", raw)
83
+ return DEFAULT_MAX_AGE_S
84
+
85
+
66
86
  def spool_path() -> Path:
67
87
  """``~/.agentos/activity.db`` — resolved at call time so tests can redirect HOME."""
68
88
  return Path.home() / ".agentos" / "activity.db"
@@ -1,4 +1,4 @@
1
- """Ship the activity spool to devops-bot-ai every 5 minutes.
1
+ """Ship the activity spool to devops-bot-ai every minute, newest first.
2
2
 
3
3
  Same shape as ``sdk.vps_health``: an asyncio loop with a ``start()`` the UI
4
4
  server's lifespan calls, cancelled by the host on shutdown.
@@ -25,8 +25,10 @@ from __future__ import annotations
25
25
 
26
26
  import asyncio
27
27
  import logging
28
+ import time
28
29
  from collections.abc import Awaitable, Callable
29
30
  from dataclasses import dataclass
31
+ from datetime import datetime, timezone
30
32
  from typing import Any, Protocol
31
33
 
32
34
  from sdk.activity import settings
@@ -176,12 +178,82 @@ async def flush_once(
176
178
  return result
177
179
 
178
180
 
181
+ def discard_spool(spool: Spool | None = None) -> int:
182
+ """Delete everything queued, without sending it. Returns the rows dropped."""
183
+ spool = spool or settings.get_spool()
184
+ dropped = 0
185
+ while True:
186
+ chunk = spool.next_chunk()
187
+ if not chunk:
188
+ return dropped
189
+ dropped += len(chunk)
190
+ spool.ack(chunk)
191
+
192
+
193
+ def purge_stale_rows(spool: Spool | None = None, now: float | None = None) -> int:
194
+ """Discard what is older than ``settings.max_age_seconds()``. Returns rows removed."""
195
+ max_age = settings.max_age_seconds()
196
+ if max_age <= 0:
197
+ return 0
198
+ spool = spool or settings.get_spool()
199
+ cutoff = (now if now is not None else time.time()) - max_age
200
+ cutoff_iso = (datetime.fromtimestamp(cutoff, timezone.utc)
201
+ .isoformat(timespec="milliseconds").replace("+00:00", "Z"))
202
+ dropped = spool.purge_stale(cutoff_iso)
203
+ if dropped:
204
+ logger.info("activity.stale_discarded rows=%d older_than_s=%.0f "
205
+ "(live tracking: only recent activity is shipped)", dropped, max_age)
206
+ return dropped
207
+
208
+
209
+ async def ship_tick(
210
+ is_vps: Callable[[], Awaitable[bool | None]] | None = None,
211
+ ) -> FlushResult | None:
212
+ """One cron tick: ship only from the caller's assigned VPS, newest first.
213
+
214
+ The relay files every batch under the TOKEN's VPS, so a laptop running the SDK
215
+ with the same token put its own events in the VPS's audit trail. On a machine
216
+ that is definitely not the VPS the queue is discarded — kept, it would only grow
217
+ forever, since that machine will never be allowed to send it. When the answer
218
+ is unknown (profile unreachable) everything recent is kept for the next tick.
219
+
220
+ Every tick first discards events older than ``AGENTOS_ACTIVITY_MAX_AGE_S``
221
+ (default one hour): the trail is for live tracking, and a backlog — a host
222
+ that was offline, or flooded by a brute-force attack — must never hold the
223
+ current activity back.
224
+ """
225
+ try:
226
+ purge_stale_rows()
227
+ except Exception as exc: # noqa: BLE001 — a purge failure must not stop shipping
228
+ logger.warning("activity: stale purge failed: %s", exc)
229
+ if is_vps is None:
230
+ from sdk import vps_identity
231
+ is_vps = vps_identity.is_assigned_vps
232
+ try:
233
+ verdict = await is_vps()
234
+ except Exception: # noqa: BLE001 — is_assigned_vps never raises, but be certain
235
+ verdict = None
236
+ if verdict is None:
237
+ return None
238
+ if verdict is False:
239
+ try:
240
+ dropped = discard_spool()
241
+ except Exception as exc: # noqa: BLE001
242
+ logger.warning("activity: could not discard the spool: %s", exc)
243
+ return None
244
+ if dropped:
245
+ logger.info("activity.not_assigned_vps discarded=%d (this machine is not the "
246
+ "user's VPS; nothing is shipped from it)", dropped)
247
+ return None
248
+ return await flush_once()
249
+
250
+
179
251
  async def run_forever(interval_s: float | None = None, *, initial_delay_s: float = 30.0) -> None:
180
- """Flush every ``interval_s`` (default 5 minutes) until cancelled."""
252
+ """Flush every ``interval_s`` (default 60 s) until cancelled."""
181
253
  await asyncio.sleep(initial_delay_s)
182
254
  while True:
183
255
  try:
184
- await flush_once()
256
+ await ship_tick()
185
257
  every = interval_s if interval_s is not None else settings.ship_interval_seconds()
186
258
  await asyncio.sleep(every)
187
259
  except asyncio.CancelledError:
@@ -169,33 +169,68 @@ class Spool:
169
169
  max_sessions: int = MAX_SESSIONS_PER_CHUNK,
170
170
  max_bytes: int = MAX_CHUNK_BYTES,
171
171
  ) -> Chunk:
172
- """The oldest rows that fit the per-request limits, in arrival order.
173
-
174
- Arrival order matters: a session is queued before its first event, so
175
- taking rows by ``seq`` ships a session no later than the events that
176
- reference it. Does NOT remove anything — call :meth:`ack` after delivery.
172
+ """Queued sessions, then the NEWEST events, within the per-request limits.
173
+
174
+ Live tracking needs the latest activity first. Shipping oldest-first left
175
+ busy hosts (a brute-forced RDP port alone is thousands of events an hour)
176
+ days behind, so the dashboard showed Tuesday's events on Thursday. Sessions
177
+ still go first, in arrival order: they are few, and an event must never
178
+ reach the relay ahead of the session it references. Does NOT remove
179
+ anything — call :meth:`ack` after delivery.
177
180
  """
178
181
  chunk = Chunk()
179
- n_events = n_sessions = size = 0
182
+ size = 0
180
183
  with self._lock:
181
- rows = self._db.execute(
182
- "SELECT seq, rev, kind, payload FROM spool ORDER BY seq LIMIT ?",
183
- (max_events + max_sessions,),
184
+ sessions = self._db.execute(
185
+ "SELECT seq, rev, kind, payload FROM spool WHERE kind = 'session' "
186
+ "ORDER BY seq LIMIT ?",
187
+ (max_sessions,),
184
188
  ).fetchall()
185
- for seq, rev, kind, payload in rows:
186
- if kind == "event" and n_events >= max_events:
187
- break # keep strict order: stop rather than skip ahead
188
- if kind == "session" and n_sessions >= max_sessions:
189
- break
189
+ events = self._db.execute(
190
+ "SELECT seq, rev, kind, payload FROM spool WHERE kind = 'event' "
191
+ "ORDER BY seq DESC LIMIT ?",
192
+ (max_events,),
193
+ ).fetchall()
194
+ for seq, rev, kind, payload in [*sessions, *events]:
190
195
  if chunk.rows and size + len(payload) > max_bytes:
191
196
  break
192
197
  # Re-fit on the way out too: rows queued by an older SDK predate the clamp.
193
198
  chunk.rows.append(Row(seq, rev, kind, fit_wire_limits(kind, json.loads(payload))))
194
- n_events += kind == "event"
195
- n_sessions += kind == "session"
196
199
  size += len(payload)
197
200
  return chunk
198
201
 
202
+ def purge_stale(self, cutoff_iso: str) -> int:
203
+ """Delete events that happened before ``cutoff_iso``, and sessions that ENDED
204
+ before it. Returns the rows removed.
205
+
206
+ For live tracking an event an hour old is history, and shipping it only
207
+ delays the current ones. Open sessions are kept whatever their age — a
208
+ login that is still going on is live state. ``cutoff_iso`` uses the wire
209
+ format (``2026-10-22T07:06:01.971Z``), so string order is time order.
210
+ """
211
+ with self._lock:
212
+ try:
213
+ cur = self._db.execute(
214
+ "DELETE FROM spool WHERE "
215
+ "(kind = 'event' AND json_extract(payload, '$.occurredAt') < ?) OR "
216
+ "(kind = 'session' AND json_extract(payload, '$.endedAt') IS NOT NULL "
217
+ " AND json_extract(payload, '$.endedAt') < ?)",
218
+ (cutoff_iso, cutoff_iso),
219
+ )
220
+ return int(cur.rowcount or 0)
221
+ except sqlite3.OperationalError:
222
+ # SQLite built without JSON1: filter in Python instead.
223
+ stale = []
224
+ for seq, kind, payload in self._db.execute(
225
+ "SELECT seq, kind, payload FROM spool"
226
+ ).fetchall():
227
+ wire = json.loads(payload)
228
+ when = wire.get("occurredAt") if kind == "event" else wire.get("endedAt")
229
+ if when and str(when) < cutoff_iso:
230
+ stale.append((seq,))
231
+ self._db.executemany("DELETE FROM spool WHERE seq = ?", stale)
232
+ return len(stale)
233
+
199
234
  # ── ack ──────────────────────────────────────────────────────────────────
200
235
 
201
236
  def ack(self, chunk: Chunk) -> None:
@@ -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)
@@ -31,8 +31,15 @@ def _isolated(tmp_path, monkeypatch):
31
31
 
32
32
 
33
33
  def _queued():
34
+ """Queued sessions and events in the order the collector RECORDED them.
35
+
36
+ The shipper sends newest-first (live tracking); these tests are about what a
37
+ collector recorded and in which order, so read the rows back by ``seq``.
38
+ """
34
39
  chunk = settings.get_spool().next_chunk()
35
- return chunk.sessions, chunk.events
40
+ rows = sorted(chunk.rows, key=lambda r: r.seq)
41
+ return ([r.wire for r in rows if r.kind == "session"],
42
+ [r.wire for r in rows if r.kind == "event"])
36
43
 
37
44
 
38
45
  # ─────────────────────────────── Linux / auditd ───────────────────────────────