devops-bot-sdk 1.6.49__tar.gz → 1.6.52__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 (223) hide show
  1. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/devops_bot_sdk.egg-info/SOURCES.txt +2 -0
  4. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/collectors/linux_auditd.py +31 -2
  6. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/server.py +6 -1
  7. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/service.py +69 -0
  8. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/setup_manager.py +3 -0
  9. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/updater.py +31 -4
  10. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/vps_health.py +17 -1
  11. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_activity_collectors.py +62 -0
  12. devops_bot_sdk-1.6.52/tests/test_stale_panel_restart.py +93 -0
  13. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_ui_lifespan.py +3 -1
  14. devops_bot_sdk-1.6.52/tests/test_updater_pep668.py +53 -0
  15. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_vps_health.py +11 -2
  16. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/README.md +0 -0
  17. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  18. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  19. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/devops_bot_sdk.egg-info/requires.txt +0 -0
  20. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  21. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/pyproject.toml +0 -0
  22. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/__init__.py +0 -0
  23. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/classifier.py +0 -0
  24. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/collectors/__init__.py +0 -0
  25. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/collectors/common.py +0 -0
  26. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/collectors/macos_eslogger.py +0 -0
  27. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/collectors/windows_eventlog.py +0 -0
  28. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/models.py +0 -0
  29. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/os_setup.py +0 -0
  30. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/recorder.py +0 -0
  31. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/redact.py +0 -0
  32. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/settings.py +0 -0
  33. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/shipper.py +0 -0
  34. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/activity/spool.py +0 -0
  35. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agentd.py +0 -0
  36. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/__init__.py +0 -0
  37. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/activity.py +0 -0
  38. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/builder.py +0 -0
  39. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/chat.py +0 -0
  40. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/connectors.py +0 -0
  41. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/engines/__init__.py +0 -0
  42. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/engines/claude_code.py +0 -0
  43. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/engines/http.py +0 -0
  44. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/engines/mcp.py +0 -0
  45. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/engines/n8n.py +0 -0
  46. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/engines/shell.py +0 -0
  47. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/integrations.py +0 -0
  48. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/inventory.py +0 -0
  49. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/library/__init__.py +0 -0
  50. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/manifest.py +0 -0
  51. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/prompt_studio.py +0 -0
  52. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/prompt_template.py +0 -0
  53. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/prompt_vars.py +0 -0
  54. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/run_log.py +0 -0
  55. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/schedules.py +0 -0
  56. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/secrets.py +0 -0
  57. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/ticket_run.py +0 -0
  58. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/usage.py +0 -0
  59. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/agents/varstore.py +0 -0
  60. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/browser_profile.py +0 -0
  61. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/cli.py +0 -0
  62. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/client.py +0 -0
  63. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/collectors/__init__.py +0 -0
  64. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/collectors/files.py +0 -0
  65. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/collectors/process.py +0 -0
  66. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/collectors/screenshot.py +0 -0
  67. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/config.py +0 -0
  68. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/crucial.py +0 -0
  69. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/departments.py +0 -0
  70. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/design_verify/__init__.py +0 -0
  71. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/design_verify/assets.py +0 -0
  72. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/design_verify/bootstrap.py +0 -0
  73. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/design_verify/browser.py +0 -0
  74. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/design_verify/compare.py +0 -0
  75. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/design_verify/loop.py +0 -0
  76. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/estimator.py +0 -0
  77. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/exceptions.py +0 -0
  78. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/firewall.py +0 -0
  79. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/formats.py +0 -0
  80. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/git_ops.py +0 -0
  81. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/github_fetch.py +0 -0
  82. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/github_ingest.py +0 -0
  83. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/graph_recon.py +0 -0
  84. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/graphify.py +0 -0
  85. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/hooks/__init__.py +0 -0
  86. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/hooks/crucial_guard.py +0 -0
  87. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/intake.py +0 -0
  88. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ipc/__init__.py +0 -0
  89. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ipc/electron_bridge.py +0 -0
  90. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ipc/handlers.py +0 -0
  91. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/journal.py +0 -0
  92. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/local_exec.py +0 -0
  93. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/memory_guard.py +0 -0
  94. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/models/__init__.py +0 -0
  95. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/models/envelope.py +0 -0
  96. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/models/requests.py +0 -0
  97. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/models/responses.py +0 -0
  98. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/models/snapshots.py +0 -0
  99. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/phase_timer.py +0 -0
  100. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/plan_run.py +0 -0
  101. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/platform_compat.py +0 -0
  102. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/prd_progress.py +0 -0
  103. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/proof_readers.py +0 -0
  104. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/providers.py +0 -0
  105. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/py.typed +0 -0
  106. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/resume.py +0 -0
  107. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/router.py +0 -0
  108. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/run_auto.py +0 -0
  109. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/scheduler.py +0 -0
  110. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/self_report.py +0 -0
  111. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/self_report_local.py +0 -0
  112. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/sse.py +0 -0
  113. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/swagger.py +0 -0
  114. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/task_file.py +0 -0
  115. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/task_types.py +0 -0
  116. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/test.py +0 -0
  117. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/test_pipeline.py +0 -0
  118. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ticket_fields.py +0 -0
  119. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/tiers/__init__.py +0 -0
  120. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/tiers/mini_agent.py +0 -0
  121. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/__init__.py +0 -0
  122. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/api.py +0 -0
  123. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/dashboard.py +0 -0
  124. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/health.py +0 -0
  125. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/ip_allowlist.py +0 -0
  126. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/jobs.py +0 -0
  127. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/models.py +0 -0
  128. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/runner.py +0 -0
  129. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/security.py +0 -0
  130. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/session.py +0 -0
  131. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/agents.html +0 -0
  132. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/app.js +0 -0
  133. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/chat.html +0 -0
  134. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/custom-agents.html +0 -0
  135. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/default-agents.html +0 -0
  136. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/index.html +0 -0
  137. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/report.html +0 -0
  138. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/runs.html +0 -0
  139. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/schedules.html +0 -0
  140. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/setup.html +0 -0
  141. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/styles.css +0 -0
  142. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/static/tickets.html +0 -0
  143. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/ui/swagger.py +0 -0
  144. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/url_access.py +0 -0
  145. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/sdk/write_hook.py +0 -0
  146. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/setup.cfg +0 -0
  147. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_activity_core.py +0 -0
  148. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_activity_macos.py +0 -0
  149. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_activity_os_setup.py +0 -0
  150. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_activity_pipeline.py +0 -0
  151. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_agent_activity.py +0 -0
  152. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_agent_activity_tracking.py +0 -0
  153. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_agent_chat_features.py +0 -0
  154. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_agent_listing.py +0 -0
  155. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_agent_prompt_async.py +0 -0
  156. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_agent_prompt_models.py +0 -0
  157. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_agent_run_ticket.py +0 -0
  158. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_agent_schedules.py +0 -0
  159. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_agent_workspace_settings.py +0 -0
  160. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_agents_connectors_builder.py +0 -0
  161. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_allowed_tools_config.py +0 -0
  162. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_auto_agent_dispatch.py +0 -0
  163. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_browser_profile.py +0 -0
  164. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_cheap_tier.py +0 -0
  165. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_client_jira_projects.py +0 -0
  166. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_config_token_diagnostics.py +0 -0
  167. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_configure_token_owner.py +0 -0
  168. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_console_encoding.py +0 -0
  169. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_crash_recovery.py +0 -0
  170. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_departments.py +0 -0
  171. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_done_transition.py +0 -0
  172. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_epic_decompose_run.py +0 -0
  173. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_epic_inherited_agent_id.py +0 -0
  174. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_epic_session_isolation.py +0 -0
  175. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_estimator.py +0 -0
  176. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_firewall_open_on_update.py +0 -0
  177. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_formats.py +0 -0
  178. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_git_action.py +0 -0
  179. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_github_fetch.py +0 -0
  180. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_github_ingest.py +0 -0
  181. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_graph_recon.py +0 -0
  182. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_graphify_build.py +0 -0
  183. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_graphify_hook_install.py +0 -0
  184. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_health.py +0 -0
  185. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_intake_job_outcome.py +0 -0
  186. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_intake_plan_review.py +0 -0
  187. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_journal.py +0 -0
  188. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_local_exec_provider.py +0 -0
  189. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_mcp_route_contract.py +0 -0
  190. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_memory_guard.py +0 -0
  191. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_phase_timer.py +0 -0
  192. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_plan_new_command.py +0 -0
  193. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_plan_run_activity.py +0 -0
  194. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_pr_identity.py +0 -0
  195. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_prd_artifact.py +0 -0
  196. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_project_path_resolution.py +0 -0
  197. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_proof_readers.py +0 -0
  198. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_providers.py +0 -0
  199. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_recon_session_isolation.py +0 -0
  200. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_resume.py +0 -0
  201. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_router.py +0 -0
  202. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_run_auto_agent_only.py +0 -0
  203. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_run_outputs.py +0 -0
  204. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_run_verdict_is_honest.py +0 -0
  205. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_schedule_api.py +0 -0
  206. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_schedule_outputs_and_plans.py +0 -0
  207. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_scheduler.py +0 -0
  208. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_self_report.py +0 -0
  209. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_service_token_warning.py +0 -0
  210. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_swagger_command.py +0 -0
  211. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_task_file_and_plan_run.py +0 -0
  212. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_task_file_format_doc.py +0 -0
  213. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_ticket_fields.py +0 -0
  214. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_ticket_run_resolve_by_id.py +0 -0
  215. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_ticket_speed_fixes.py +0 -0
  216. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_tier_flag_parity.py +0 -0
  217. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_ui_gate.py +0 -0
  218. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_update_timer.py +0 -0
  219. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_updater_extras.py +0 -0
  220. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_updater_reports_the_truth.py +0 -0
  221. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_url_access.py +0 -0
  222. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/tests/test_windows_loop_identity.py +0 -0
  223. {devops_bot_sdk-1.6.49 → devops_bot_sdk-1.6.52}/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.49
3
+ Version: 1.6.52
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.49
3
+ Version: 1.6.52
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
@@ -201,6 +201,7 @@ tests/test_schedule_outputs_and_plans.py
201
201
  tests/test_scheduler.py
202
202
  tests/test_self_report.py
203
203
  tests/test_service_token_warning.py
204
+ tests/test_stale_panel_restart.py
204
205
  tests/test_swagger_command.py
205
206
  tests/test_task_file_and_plan_run.py
206
207
  tests/test_task_file_format_doc.py
@@ -212,6 +213,7 @@ tests/test_ui_gate.py
212
213
  tests/test_ui_lifespan.py
213
214
  tests/test_update_timer.py
214
215
  tests/test_updater_extras.py
216
+ tests/test_updater_pep668.py
215
217
  tests/test_updater_reports_the_truth.py
216
218
  tests/test_url_access.py
217
219
  tests/test_vps_health.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.49
3
+ Version: 1.6.52
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.49" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.52" # 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
@@ -52,6 +52,11 @@ DEFAULT_AUDIT_LOG = "/var/log/audit/audit.log"
52
52
  _UNSET = {"4294967295", "-1", "unset"}
53
53
  #: Records of one event share a serial; bound how many half-seen events we hold.
54
54
  _MAX_PENDING = 512
55
+ #: An event whose records stopped arriving this long ago is complete. auditd 4.x
56
+ #: (Ubuntu 26.04) writes NO `EOE` records at all, so this, `PROCTITLE` and the
57
+ #: single-record user-space types are what close an event there. Mirrors auparse's
58
+ #: own `eoe_timeout` (2 s by default).
59
+ EOE_TIMEOUT_S = 2.0
55
60
  _MAX_PID_CACHE = 20_000
56
61
  #: ``acct`` values on a failed login that are placeholders, not an account name.
57
62
  _NOT_A_NAME = ("(unknown)", "(invalid user)", "?")
@@ -139,6 +144,7 @@ class AuditdParser:
139
144
  self._user_name = user_name
140
145
  self._book = book or SessionBook()
141
146
  self._pending: OrderedDict[str, dict[str, dict[str, str]]] = OrderedDict()
147
+ self._pending_ts: dict[str, float] = {} # serial → event time
142
148
  self._ppid: OrderedDict[int, int] = OrderedDict()
143
149
  self._ses_key: dict[str, str] = {} # auditd ses id → our session key
144
150
  self._ses_kind: dict[str, SessionKind] = {}
@@ -177,6 +183,15 @@ class AuditdParser:
177
183
  rec["_args"] = " ".join(args[i] for i in sorted(args))
178
184
 
179
185
  self._pending.setdefault(serial, {})[rtype] = rec
186
+ self._pending_ts.setdefault(serial, ts)
187
+ # Close the event as soon as it is known to be complete, rather than only on
188
+ # EOE: auditd 4.x never writes EOE, and an event held for one waited for up to
189
+ # 512 later events (a quiet box shipped nothing; a restart lost what was held).
190
+ if rtype.startswith("USER_") or rtype == "PROCTITLE":
191
+ # USER_* come from user space as one self-contained record; PROCTITLE is
192
+ # the kernel's last record of a syscall event.
193
+ self._finalize(serial)
194
+ self.flush_older_than(ts - EOE_TIMEOUT_S)
180
195
  while len(self._pending) > _MAX_PENDING:
181
196
  self._finalize(next(iter(self._pending)))
182
197
 
@@ -185,10 +200,21 @@ class AuditdParser:
185
200
  for serial in list(self._pending):
186
201
  self._finalize(serial)
187
202
 
203
+ def flush_older_than(self, cutoff: float) -> None:
204
+ """Finalize events whose time is before ``cutoff`` — their records are all in.
205
+
206
+ Called with the newest event time on every record, and with the wall clock by
207
+ the tail loop when the log is idle, so the last event before a quiet spell is
208
+ not held until the next one arrives.
209
+ """
210
+ for serial in [s for s, t in self._pending_ts.items() if t < cutoff]:
211
+ self._finalize(serial)
212
+
188
213
  # ── one event ────────────────────────────────────────────────────────────
189
214
 
190
215
  def _finalize(self, serial: str) -> None:
191
216
  recs = self._pending.pop(serial, None)
217
+ self._pending_ts.pop(serial, None)
192
218
  if not recs:
193
219
  return
194
220
  try:
@@ -396,9 +422,12 @@ class AuditdCollector:
396
422
  for raw in data[:cut].splitlines():
397
423
  self._parser.feed(raw.decode("utf-8", "replace"))
398
424
  offset += cut
399
- # No parser.flush() here: an event is finalized by its EOE record, and
400
- # flushing mid-read could finalize one whose lines straddle two reads.
425
+ # No parser.flush() here: flushing mid-read could finalize an event whose
426
+ # lines straddle two reads. Only events older than EOE_TIMEOUT_S are
427
+ # closed below — their records are all in the log by then.
401
428
  self._save(inode, offset)
429
+ # Wall-clock close for the last event before a quiet spell (no EOE on auditd 4.x).
430
+ self._parser.flush_older_than(time.time() - EOE_TIMEOUT_S)
402
431
  time.sleep(self._poll_s)
403
432
 
404
433
  def _load(self) -> tuple[int | None, int]:
@@ -140,7 +140,12 @@ def create_app():
140
140
 
141
141
  @app.get("/healthz")
142
142
  async def healthz():
143
- return {"ok": True}
143
+ # `version` is what the updater compares against the installed SDK: a panel
144
+ # started before an upgrade keeps running the old code until it restarts,
145
+ # and "already up to date" on disk says nothing about the running process.
146
+ from sdk import __version__ as _v
147
+
148
+ return {"ok": True, "version": _v}
144
149
 
145
150
  # ── Swagger: public shell, token-gated schema ───────────────────────────────
146
151
  # Registered HERE, before the static mount below: Starlette matches in insertion
@@ -678,6 +678,71 @@ def refresh() -> tuple[bool, str]:
678
678
  else f"service definition regenerated ({host}:{port}) but restart needs attention: {rmsg}")
679
679
 
680
680
 
681
+ def running_version(timeout: float = 3.0) -> str | None:
682
+ """The SDK version the RUNNING panel reports, from its own ``/healthz``.
683
+
684
+ ``""`` when it answers without a version (a panel older than this check: stale
685
+ by definition), ``None`` when it cannot be reached (unknown: do not act on it).
686
+ Loopback unless the panel was bound to one specific address.
687
+ """
688
+ import json
689
+ import urllib.request
690
+
691
+ host, port = _installed_host_port()
692
+ if host in ("0.0.0.0", "::", ""):
693
+ host = "127.0.0.1"
694
+ url = f"http://{'[' + host + ']' if ':' in host else host}:{port}/healthz"
695
+ try:
696
+ with urllib.request.urlopen(url, timeout=timeout) as resp: # noqa: S310 — loopback/own panel
697
+ body = json.loads(resp.read().decode("utf-8", "replace") or "{}")
698
+ except Exception: # noqa: BLE001 — not up yet, other port, IP gate …
699
+ return None
700
+ return str(body.get("version") or "")
701
+
702
+
703
+ def _wait_for_version(want: str, attempts: int = 10, delay: float = 2.0) -> str | None:
704
+ """Poll the restarted panel until it answers; its version, or None if it never did."""
705
+ import time
706
+
707
+ seen: str | None = None
708
+ for _ in range(attempts):
709
+ seen = running_version()
710
+ if seen == want:
711
+ return seen
712
+ time.sleep(delay)
713
+ return seen
714
+
715
+
716
+ def _restart_if_stale() -> tuple[bool, str] | None:
717
+ """Restart a running panel whose code is older than the installed SDK.
718
+
719
+ The gap this closes: when ``agentos-auto update`` finds the disk already current
720
+ it never restarted the panel, so a panel started BEFORE the upgrade landed (by
721
+ an earlier update run, a manual pip install, a second install) kept running the
722
+ old code indefinitely. On a VPS that meant the activity collector never started.
723
+ ``None`` when there is nothing to do (current, or the version is unknowable).
724
+ """
725
+ from sdk import __version__ as installed
726
+
727
+ running = running_version()
728
+ if running is None or running == installed:
729
+ return None
730
+ was = running or "a version older than 1.6.50"
731
+ rok, rmsg = refresh()
732
+ if not rok:
733
+ return False, f"panel runs {was} but {installed} is installed; restart failed: {rmsg}"
734
+ now = _wait_for_version(installed)
735
+ if now == installed:
736
+ return True, f"panel was running {was}; restarted on {installed}."
737
+ # Restarted, and STILL not this version: the service runs another installation
738
+ # (a different interpreter / prefix) that this updater did not touch.
739
+ unit = _service_definition_path()
740
+ return False, (f"panel restarted but reports {now or 'no version'}, not {installed}: the "
741
+ f"service runs a different SDK installation than this one "
742
+ f"({sys.executable}). Check the service definition"
743
+ f"{f' ({unit})' if unit else ''} and update that installation.")
744
+
745
+
681
746
  def _service_disabled() -> bool:
682
747
  return os.environ.get("AGENTOS_NO_SERVICE", "").strip().lower() in ("1", "true", "yes", "on")
683
748
 
@@ -719,6 +784,10 @@ def ensure(restart_running: bool = False) -> tuple[bool, str]:
719
784
  rok, rmsg = refresh()
720
785
  return rok, (_with_token_warning(rmsg) if rok else rmsg)
721
786
  if _is_active():
787
+ stale = _restart_if_stale()
788
+ if stale is not None:
789
+ ok, msg = stale
790
+ return ok, (_with_token_warning(msg) if ok else msg)
722
791
  return True, _with_token_warning("service already running.")
723
792
  rok, rmsg = restart() # installed but down → bring it back up
724
793
  return rok, (_with_token_warning(f"service was down — started; {rmsg}") if rok else rmsg)
@@ -102,9 +102,12 @@ async def reinstall_sdk(upgrade: bool = True):
102
102
  which is the managed ``agentsystem`` venv in normal operation. Used by
103
103
  ``/setup`` for a repair/reinstall.
104
104
  """
105
+ from sdk.updater import _pip_flags
106
+
105
107
  argv = [sys.executable, "-m", "pip", "install"]
106
108
  if upgrade:
107
109
  argv.append("--upgrade")
110
+ argv += _pip_flags()
108
111
  argv.append(SDK_GIT_SPEC)
109
112
 
110
113
  yield f"$ {' '.join(argv)}\n"
@@ -8,7 +8,8 @@ Upgrade priority:
8
8
  (backend returns the user's stored GitHub OAuth token — no extra config)
9
9
  4. PyPI dist name as last resort
10
10
 
11
- Adds --break-system-packages automatically on Linux (Ubuntu 22.04+/Debian 12+).
11
+ Adds --break-system-packages automatically on any externally-managed Python
12
+ (PEP 668: Ubuntu 22.04+/Debian 12+, Homebrew Python on macOS).
12
13
  Disable entirely with AGENTOS_NO_UPDATE=1.
13
14
  """
14
15
  from __future__ import annotations
@@ -54,9 +55,35 @@ def _with_extras(target: str) -> str:
54
55
 
55
56
  # ── Platform helpers ──────────────────────────────────────────────────────────
56
57
 
58
+ def _externally_managed() -> bool:
59
+ """True when pip will refuse this interpreter under PEP 668.
60
+
61
+ The marker is an ``EXTERNALLY-MANAGED`` file in the stdlib directory, and a
62
+ venv is exempt. Checking for it, rather than the OS, is what covers Homebrew
63
+ Python on macOS: a Mac VPS on ``/opt/homebrew/.../python3.14`` was refused on
64
+ every upgrade attempt and stayed on an old version indefinitely, because the
65
+ flag below used to be added on Linux only.
66
+ """
67
+ if sys.prefix != getattr(sys, "base_prefix", sys.prefix):
68
+ return False
69
+ try:
70
+ import sysconfig
71
+ return os.path.isfile(
72
+ os.path.join(sysconfig.get_path("stdlib"), "EXTERNALLY-MANAGED"))
73
+ except Exception: # noqa: BLE001 — an unreadable layout is not "managed"
74
+ return False
75
+
76
+
57
77
  def _pip_flags() -> list[str]:
58
- """--break-system-packages on Linux (PEP 668); empty elsewhere."""
59
- return ["--break-system-packages"] if platform.system() == "Linux" else []
78
+ """--break-system-packages where PEP 668 applies; empty elsewhere.
79
+
80
+ Linux keeps the flag unconditionally, as before (every supported distro pip
81
+ accepts it). Elsewhere it is added only when the marker is present, since an
82
+ old pip that predates PEP 668 rejects the option outright.
83
+ """
84
+ if platform.system() == "Linux" or _externally_managed():
85
+ return ["--break-system-packages"]
86
+ return []
60
87
 
61
88
 
62
89
  def _vtuple(v: str) -> tuple:
@@ -589,7 +616,7 @@ def _report(result: UpgradeResult, latest: str | None) -> None:
589
616
  _say(f" pip said: {result.detail}")
590
617
  _say(" fix it by hand with:\n"
591
618
  f' "{sys.executable}" -m pip install --upgrade --force-reinstall '
592
- f'--no-cache-dir "{_DIST}{_EXTRAS}"')
619
+ f'--no-cache-dir {" ".join(_pip_flags() + [""])}"{_DIST}{_EXTRAS}"')
593
620
  return
594
621
 
595
622
  if result.ran:
@@ -33,6 +33,7 @@ ping just waits for the next tick.
33
33
  from __future__ import annotations
34
34
 
35
35
  import asyncio
36
+ import ipaddress
36
37
  import logging
37
38
  import os
38
39
  import socket
@@ -120,6 +121,21 @@ def _outbound_ip() -> str:
120
121
  pass
121
122
 
122
123
 
124
+ def _is_public(ip: str) -> bool:
125
+ """Only a globally-routable address may overwrite the allocation's IP.
126
+
127
+ Behind NAT (an AWS instance, a laptop on Wi-Fi) the route-to-internet address
128
+ is private — ``172.31.x.x``, ``192.168.x.x`` — and sending it replaced the
129
+ VPS's real public IP on ``contabo_allocation``, after which the backend could
130
+ no longer reach the panel at all. Omitting the key leaves the stored value
131
+ alone, the same rule every other unavailable metric here follows.
132
+ """
133
+ try:
134
+ return bool(ip) and ipaddress.ip_address(ip).is_global
135
+ except ValueError:
136
+ return False
137
+
138
+
123
139
  def _bandwidth_used_mb() -> float | None:
124
140
  """Total bytes in+out since boot, in MB. None when psutil is unavailable.
125
141
 
@@ -183,7 +199,7 @@ def build_payload(health: dict | None = None) -> dict:
183
199
  payload["bandwidthUsed"] = bw # MB, cumulative
184
200
 
185
201
  ip = _outbound_ip()
186
- if ip:
202
+ if _is_public(ip):
187
203
  payload["ipAddress"] = ip
188
204
 
189
205
  return payload
@@ -647,3 +647,65 @@ class TestSetupAtPanelStart:
647
647
  async def go():
648
648
  await collectors.start()
649
649
  asyncio.run(go())
650
+
651
+
652
+ class TestNoEoeRecords:
653
+ """auditd 4.x (Ubuntu 26.04) writes NO `EOE` records. The parser closed events only
654
+ on EOE (or after 512 more piled up), so a quiet VPS shipped nothing at all and a busy
655
+ one shipped ~10% of its commands, late, losing whatever was held at a restart."""
656
+
657
+ # Verbatim from a live Ubuntu 26.04 / auditctl 4.1.2 VPS (log_format = ENRICHED; the
658
+ # enriched tail is separated by \x1d), serial 761 — no EOE follows it.
659
+ JAVAID_EXEC = [
660
+ 'type=SYSCALL msg=audit(1791570760.040:761): arch=c000003e syscall=59 success=yes exit=0 '
661
+ 'a0=7bd5038c3130 a1=7bd5038c30d0 a2=7bd5038f77b0 a3=8 items=2 ppid=1387309 pid=1388791 '
662
+ 'auid=4294967295 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=(none) '
663
+ 'ses=4294967295 comm="auditctl" exe="/usr/sbin/auditctl" subj=unconfined '
664
+ 'key="agentos_exec"\x1dARCH=x86_64 SYSCALL=execve AUID="unset" UID="root"',
665
+ 'type=EXECVE msg=audit(1791570760.040:761): argc=2 a0="auditctl" a1="-l"',
666
+ 'type=CWD msg=audit(1791570760.040:761): cwd="/root"',
667
+ 'type=PATH msg=audit(1791570760.040:761): item=0 name="/usr/sbin/auditctl" inode=60036 '
668
+ 'dev=fd:01 mode=0100755 ouid=0 ogid=0 rdev=00:00 nametype=NORMAL',
669
+ 'type=PROCTITLE msg=audit(1791570760.040:761): proctitle=2F7573722F7362696E2F61756469746374'
670
+ '6C002D6C',
671
+ ]
672
+
673
+ def test_a_command_is_recorded_without_an_eoe_line(self):
674
+ p = _parser()
675
+ _feed(p, self.JAVAID_EXEC)
676
+ _, events = _queued()
677
+ assert [(e["action"], e["command"]) for e in events] == [("exec", "auditctl -l")]
678
+
679
+ def test_without_proctitle_the_event_closes_once_a_later_event_is_two_seconds_on(self):
680
+ p = _parser()
681
+ _feed(p, self.JAVAID_EXEC[:4]) # no PROCTITLE, no EOE
682
+ assert _queued()[1] == [] # may still be arriving
683
+ _feed(p, ['type=CWD msg=audit(1791570762.500:900): cwd="/"']) # 2.46 s later
684
+ _, events = _queued()
685
+ assert [e["command"] for e in events] == ["auditctl -l"]
686
+
687
+ def test_the_idle_tail_closes_the_last_event_by_wall_clock(self):
688
+ p = _parser()
689
+ _feed(p, self.JAVAID_EXEC[:4])
690
+ assert _queued()[1] == []
691
+ p.flush_older_than(1791570760.040 + L.EOE_TIMEOUT_S + 0.1)
692
+ assert [e["command"] for e in _queued()[1]] == ["auditctl -l"]
693
+
694
+ def test_a_failed_login_is_recorded_on_its_own_record(self):
695
+ """USER_* records are single, self-contained user-space events: no EOE ever."""
696
+ p = _parser(user_name=L._default_user_name)
697
+ _feed(p, [
698
+ "type=USER_LOGIN msg=audit(1791568160.154:240124): pid=1828747 uid=0 "
699
+ "auid=4294967295 ses=4294967295 subj=unconfined msg='op=login "
700
+ "acct=\"root\" exe=\"/usr/lib/openssh/sshd-session\" "
701
+ "hostname=? addr=80.94.92.55 terminal=sshd res=failed'\x1dUID=\"root\" AUID=\"unset\"",
702
+ ])
703
+ _, events = _queued()
704
+ assert [(e["action"], e["osUser"], e["target"]) for e in events] == [
705
+ ("login.failed", "root", "80.94.92.55")]
706
+
707
+ def test_an_event_is_recorded_once_even_if_an_eoe_does_follow(self):
708
+ """Older auditd still writes EOE after PROCTITLE: no double record."""
709
+ p = _parser()
710
+ _feed(p, [*self.JAVAID_EXEC, "type=EOE msg=audit(1791570760.040:761): "])
711
+ assert len(_queued()[1]) == 1
@@ -0,0 +1,93 @@
1
+ """A panel started before an upgrade landed is restarted by the next update.
2
+
3
+ Regression (2026-10-09, several VPSs): `agentos-auto update` said "already up to date
4
+ (v1.6.49)" and `activity-setup --check` said "already set up", yet the box shipped no
5
+ activity — the panel had been running since before the upgrade, on old code, and
6
+ the "already current" path never restarted it.
7
+ """
8
+ from unittest.mock import patch
9
+
10
+ from sdk import __version__ as INSTALLED
11
+ from sdk.ui import service
12
+
13
+
14
+ def _ensure_running(monkeypatch, versions, refresh=(True, "service refreshed")):
15
+ """ensure() on an installed, active service; `versions` = successive /healthz answers."""
16
+ answers = iter(versions)
17
+ monkeypatch.setattr(service, "running_version", lambda timeout=3.0: next(answers, versions[-1]))
18
+ monkeypatch.setattr(service, "_wait_for_version",
19
+ lambda want, attempts=10, delay=2.0: next(answers, versions[-1]))
20
+ calls = []
21
+
22
+ def fake_refresh():
23
+ calls.append("refresh")
24
+ return refresh
25
+
26
+ with patch.object(service, "_service_disabled", return_value=False), \
27
+ patch.object(service, "is_installed", return_value=True), \
28
+ patch.object(service, "update_timer_installed", return_value=True), \
29
+ patch.object(service, "_needs_root_for_existing", return_value=False), \
30
+ patch.object(service, "_is_active", return_value=True), \
31
+ patch.object(service, "_with_token_warning", side_effect=lambda m: m), \
32
+ patch.object(service, "refresh", side_effect=fake_refresh):
33
+ ok, msg = service.ensure()
34
+ return ok, msg, calls
35
+
36
+
37
+ def test_a_current_panel_is_left_alone(monkeypatch):
38
+ ok, msg, calls = _ensure_running(monkeypatch, [INSTALLED])
39
+ assert calls == [] and ok and msg == "service already running."
40
+
41
+
42
+ def test_an_older_panel_is_restarted_onto_the_installed_version(monkeypatch):
43
+ ok, msg, calls = _ensure_running(monkeypatch, ["1.6.44", INSTALLED])
44
+ assert calls == ["refresh"] and ok
45
+ assert "1.6.44" in msg and INSTALLED in msg
46
+
47
+
48
+ def test_a_panel_too_old_to_report_a_version_is_stale(monkeypatch):
49
+ """Panels before this fix answer /healthz with no version at all."""
50
+ ok, msg, calls = _ensure_running(monkeypatch, ["", INSTALLED])
51
+ assert calls == ["refresh"] and ok
52
+
53
+
54
+ def test_an_unreachable_panel_is_never_restarted_on_a_guess(monkeypatch):
55
+ ok, msg, calls = _ensure_running(monkeypatch, [None])
56
+ assert calls == [] and msg == "service already running."
57
+
58
+
59
+ def test_still_old_after_restart_names_the_second_installation(monkeypatch):
60
+ """The VPS case where the service ran /usr/local python3.12 and the update had
61
+ upgraded a different install: say so instead of claiming success."""
62
+ ok, msg, calls = _ensure_running(monkeypatch, ["1.6.44", "1.6.44"])
63
+ assert calls == ["refresh"] and ok is False
64
+ assert "different SDK installation" in msg
65
+
66
+
67
+ def test_a_failed_restart_is_reported(monkeypatch):
68
+ ok, msg, calls = _ensure_running(monkeypatch, ["1.6.44"], refresh=(False, "permission denied"))
69
+ assert ok is False and "permission denied" in msg
70
+
71
+
72
+ def test_the_probe_reads_the_version_from_healthz(monkeypatch):
73
+ import io
74
+ import json
75
+
76
+ seen = {}
77
+
78
+ class Resp(io.BytesIO):
79
+ def __enter__(self):
80
+ return self
81
+
82
+ def __exit__(self, *a):
83
+ return False
84
+
85
+ def fake_urlopen(url, timeout):
86
+ seen["url"] = url
87
+ return Resp(json.dumps({"ok": True, "version": "9.9.9"}).encode())
88
+
89
+ monkeypatch.undo() # drop conftest's running_version stub: test the real probe
90
+ monkeypatch.setattr(service, "_installed_host_port", lambda: ("0.0.0.0", 8765))
91
+ monkeypatch.setattr("urllib.request.urlopen", fake_urlopen)
92
+ assert service.running_version() == "9.9.9"
93
+ assert seen["url"] == "http://127.0.0.1:8765/healthz"
@@ -99,4 +99,6 @@ class TestLifespanCrons:
99
99
  # Entering the context manager runs the REAL lifespan, so a cron that
100
100
  # raises there would surface as a failed startup rather than a served page.
101
101
  with TestClient(create_app()) as c:
102
- assert c.get("/healthz").json() == {"ok": True}
102
+ from sdk import __version__
103
+ # `version` lets the updater spot a panel still running pre-upgrade code.
104
+ assert c.get("/healthz").json() == {"ok": True, "version": __version__}
@@ -0,0 +1,53 @@
1
+ """PEP 668 is a property of the interpreter, not of Linux.
2
+
3
+ Homebrew Python on macOS ships an EXTERNALLY-MANAGED marker, and the updater
4
+ used to add --break-system-packages on Linux only — so a Mac VPS was refused on
5
+ every upgrade path and stayed on an old SDK indefinitely.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+
11
+ from sdk import updater
12
+
13
+
14
+ def _stdlib(monkeypatch, tmp_path, *, marker: bool, venv: bool = False):
15
+ import sysconfig
16
+ if marker:
17
+ (tmp_path / "EXTERNALLY-MANAGED").write_text("[externally-managed]\n")
18
+ monkeypatch.setattr(sysconfig, "get_path", lambda name: str(tmp_path))
19
+ monkeypatch.setattr(sys, "base_prefix", "/base" if venv else sys.prefix)
20
+
21
+
22
+ def test_homebrew_macos_gets_the_flag(monkeypatch, tmp_path):
23
+ monkeypatch.setattr(updater.platform, "system", lambda: "Darwin")
24
+ _stdlib(monkeypatch, tmp_path, marker=True)
25
+ assert updater._pip_flags() == ["--break-system-packages"]
26
+
27
+
28
+ def test_unmanaged_macos_python_does_not(monkeypatch, tmp_path):
29
+ """An old pip that predates PEP 668 rejects the option outright."""
30
+ monkeypatch.setattr(updater.platform, "system", lambda: "Darwin")
31
+ _stdlib(monkeypatch, tmp_path, marker=False)
32
+ assert updater._pip_flags() == []
33
+
34
+
35
+ def test_a_venv_is_exempt(monkeypatch, tmp_path):
36
+ monkeypatch.setattr(updater.platform, "system", lambda: "Windows")
37
+ _stdlib(monkeypatch, tmp_path, marker=True, venv=True)
38
+ assert updater._pip_flags() == []
39
+
40
+
41
+ def test_linux_keeps_the_flag_unconditionally(monkeypatch, tmp_path):
42
+ monkeypatch.setattr(updater.platform, "system", lambda: "Linux")
43
+ _stdlib(monkeypatch, tmp_path, marker=False)
44
+ assert updater._pip_flags() == ["--break-system-packages"]
45
+
46
+
47
+ def test_the_hand_fix_hint_carries_the_flag(monkeypatch, tmp_path, capsys):
48
+ monkeypatch.setattr(updater.platform, "system", lambda: "Darwin")
49
+ _stdlib(monkeypatch, tmp_path, marker=True)
50
+ res = updater.UpgradeResult(before="1.6.44", after="1.6.44", changed=False,
51
+ ran=True, detail="externally-managed-environment")
52
+ updater._report(res, latest="1.6.51")
53
+ assert "--break-system-packages" in capsys.readouterr().out
@@ -73,7 +73,7 @@ class TestBuildPayload:
73
73
  def test_maps_every_field(self, monkeypatch):
74
74
  monkeypatch.delenv("AGENTOS_VPS_HEALTH_STATUS", raising=False)
75
75
  monkeypatch.setattr(vps_health, "_bandwidth_used_mb", lambda: 1024.5)
76
- monkeypatch.setattr(vps_health, "_outbound_ip", lambda: "192.168.1.1")
76
+ monkeypatch.setattr(vps_health, "_outbound_ip", lambda: "77.68.16.238")
77
77
 
78
78
  p = vps_health.build_payload(_HEALTH)
79
79
 
@@ -85,7 +85,7 @@ class TestBuildPayload:
85
85
  assert p["diskUsage"] == 30.2
86
86
  assert p["totalDisk"] == 500.0
87
87
  assert p["bandwidthUsed"] == 1024.5
88
- assert p["ipAddress"] == "192.168.1.1"
88
+ assert p["ipAddress"] == "77.68.16.238"
89
89
  assert p["uptimeSeconds"] == 86400
90
90
  # ISO-8601 with millis + Z, as the backend sample shows.
91
91
  assert p["lastPingedAt"].endswith("Z") and "T" in p["lastPingedAt"]
@@ -118,6 +118,15 @@ class TestBuildPayload:
118
118
  "totalDisk", "uptimeSeconds", "bandwidthUsed", "ipAddress"):
119
119
  assert absent not in p
120
120
 
121
+ @pytest.mark.parametrize("ip", ["172.31.44.234", "192.168.18.16", "10.0.0.5",
122
+ "127.0.0.1", "not-an-ip"])
123
+ def test_a_private_ip_never_overwrites_the_allocation(self, monkeypatch, ip):
124
+ """Behind NAT the outbound address is private; sending it replaced the
125
+ VPS's public IP and left the backend unable to reach the panel."""
126
+ monkeypatch.setattr(vps_health, "_bandwidth_used_mb", lambda: None)
127
+ monkeypatch.setattr(vps_health, "_outbound_ip", lambda: ip)
128
+ assert "ipAddress" not in vps_health.build_payload(_HEALTH)
129
+
121
130
 
122
131
  class _FakeClient:
123
132
  def __init__(self, ok: bool = True) -> None: