devops-bot-sdk 1.6.70__tar.gz → 1.6.73__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.70 → devops_bot_sdk-1.6.73}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
  4. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/collectors/windows_eventlog.py +68 -8
  6. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/os_setup.py +4 -0
  7. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/settings.py +21 -1
  8. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/shipper.py +31 -4
  9. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/spool.py +51 -16
  10. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_activity_collectors.py +60 -1
  11. devops_bot_sdk-1.6.73/tests/test_activity_live_tracking.py +145 -0
  12. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_activity_macos.py +8 -1
  13. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_activity_pipeline.py +18 -14
  14. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/README.md +0 -0
  15. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  16. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  17. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/devops_bot_sdk.egg-info/requires.txt +0 -0
  18. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  19. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/pyproject.toml +0 -0
  20. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/__init__.py +0 -0
  21. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/classifier.py +0 -0
  22. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/collectors/__init__.py +0 -0
  23. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/collectors/common.py +0 -0
  24. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/collectors/linux_auditd.py +0 -0
  25. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/collectors/macos_eslogger.py +0 -0
  26. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/models.py +0 -0
  27. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/recorder.py +0 -0
  28. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/activity/redact.py +0 -0
  29. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agentd.py +0 -0
  30. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/__init__.py +0 -0
  31. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/activity.py +0 -0
  32. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/builder.py +0 -0
  33. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/chat.py +0 -0
  34. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/connectors.py +0 -0
  35. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/engines/__init__.py +0 -0
  36. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/engines/claude_code.py +0 -0
  37. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/engines/http.py +0 -0
  38. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/engines/mcp.py +0 -0
  39. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/engines/n8n.py +0 -0
  40. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/engines/shell.py +0 -0
  41. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/integrations.py +0 -0
  42. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/inventory.py +0 -0
  43. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/library/__init__.py +0 -0
  44. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/manifest.py +0 -0
  45. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/prompt_studio.py +0 -0
  46. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/prompt_template.py +0 -0
  47. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/prompt_vars.py +0 -0
  48. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/run_log.py +0 -0
  49. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/schedules.py +0 -0
  50. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/secrets.py +0 -0
  51. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/ticket_run.py +0 -0
  52. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/usage.py +0 -0
  53. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/agents/varstore.py +0 -0
  54. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/browser_profile.py +0 -0
  55. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/cli.py +0 -0
  56. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/client.py +0 -0
  57. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/collectors/__init__.py +0 -0
  58. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/collectors/files.py +0 -0
  59. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/collectors/process.py +0 -0
  60. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/collectors/screenshot.py +0 -0
  61. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/config.py +0 -0
  62. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/crucial.py +0 -0
  63. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/custom_agents.py +0 -0
  64. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/departments.py +0 -0
  65. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/design_verify/__init__.py +0 -0
  66. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/design_verify/assets.py +0 -0
  67. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/design_verify/bootstrap.py +0 -0
  68. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/design_verify/browser.py +0 -0
  69. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/design_verify/compare.py +0 -0
  70. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/design_verify/loop.py +0 -0
  71. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/estimator.py +0 -0
  72. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/exceptions.py +0 -0
  73. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/firewall.py +0 -0
  74. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/formats.py +0 -0
  75. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/git_ops.py +0 -0
  76. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/github_fetch.py +0 -0
  77. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/github_ingest.py +0 -0
  78. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/graph_recon.py +0 -0
  79. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/graphify.py +0 -0
  80. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/hooks/__init__.py +0 -0
  81. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/hooks/crucial_guard.py +0 -0
  82. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/intake.py +0 -0
  83. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ipc/__init__.py +0 -0
  84. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ipc/electron_bridge.py +0 -0
  85. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ipc/handlers.py +0 -0
  86. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/journal.py +0 -0
  87. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/local_exec.py +0 -0
  88. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/memory_guard.py +0 -0
  89. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/models/__init__.py +0 -0
  90. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/models/envelope.py +0 -0
  91. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/models/requests.py +0 -0
  92. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/models/responses.py +0 -0
  93. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/models/snapshots.py +0 -0
  94. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/phase_timer.py +0 -0
  95. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/plan_run.py +0 -0
  96. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/platform_compat.py +0 -0
  97. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/prd_progress.py +0 -0
  98. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/proof_readers.py +0 -0
  99. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/providers.py +0 -0
  100. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/py.typed +0 -0
  101. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/resume.py +0 -0
  102. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/router.py +0 -0
  103. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/run_auto.py +0 -0
  104. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/schedule_tickets.py +0 -0
  105. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/scheduler.py +0 -0
  106. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/self_report.py +0 -0
  107. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/self_report_local.py +0 -0
  108. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/sse.py +0 -0
  109. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/swagger.py +0 -0
  110. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/task_file.py +0 -0
  111. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/task_types.py +0 -0
  112. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/test.py +0 -0
  113. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/test_pipeline.py +0 -0
  114. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ticket_fields.py +0 -0
  115. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/tickets.py +0 -0
  116. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/tiers/__init__.py +0 -0
  117. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/tiers/mini_agent.py +0 -0
  118. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/__init__.py +0 -0
  119. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/api.py +0 -0
  120. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/dashboard.py +0 -0
  121. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/health.py +0 -0
  122. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/ip_allowlist.py +0 -0
  123. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/jobs.py +0 -0
  124. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/models.py +0 -0
  125. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/runner.py +0 -0
  126. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/security.py +0 -0
  127. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/server.py +0 -0
  128. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/service.py +0 -0
  129. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/session.py +0 -0
  130. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/setup_manager.py +0 -0
  131. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/agents.html +0 -0
  132. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/app.js +0 -0
  133. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/chat.html +0 -0
  134. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/custom-agents.html +0 -0
  135. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/default-agents.html +0 -0
  136. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/index.html +0 -0
  137. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/report.html +0 -0
  138. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/runs.html +0 -0
  139. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/schedules.html +0 -0
  140. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/setup.html +0 -0
  141. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/styles.css +0 -0
  142. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/static/tickets.html +0 -0
  143. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/ui/swagger.py +0 -0
  144. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/updater.py +0 -0
  145. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/url_access.py +0 -0
  146. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
  147. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/vps_health.py +0 -0
  148. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/vps_identity.py +0 -0
  149. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/sdk/write_hook.py +0 -0
  150. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/setup.cfg +0 -0
  151. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_activity_core.py +0 -0
  152. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_activity_os_setup.py +0 -0
  153. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_agent_activity.py +0 -0
  154. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_agent_activity_tracking.py +0 -0
  155. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_agent_chat_features.py +0 -0
  156. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_agent_crud_cli.py +0 -0
  157. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_agent_listing.py +0 -0
  158. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_agent_prompt_async.py +0 -0
  159. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_agent_prompt_models.py +0 -0
  160. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_agent_run_ticket.py +0 -0
  161. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_agent_schedules.py +0 -0
  162. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_agent_workspace_settings.py +0 -0
  163. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_agents_connectors_builder.py +0 -0
  164. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_allowed_tools_config.py +0 -0
  165. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_auto_agent_dispatch.py +0 -0
  166. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_browser_profile.py +0 -0
  167. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_cheap_tier.py +0 -0
  168. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_client_jira_projects.py +0 -0
  169. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_config_token_diagnostics.py +0 -0
  170. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_configure_token_owner.py +0 -0
  171. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_console_encoding.py +0 -0
  172. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_crash_recovery.py +0 -0
  173. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_departments.py +0 -0
  174. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_done_transition.py +0 -0
  175. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_epic_decompose_run.py +0 -0
  176. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_epic_inherited_agent_id.py +0 -0
  177. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_epic_session_isolation.py +0 -0
  178. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_estimator.py +0 -0
  179. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_fallback_cli.py +0 -0
  180. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_firewall_open_on_update.py +0 -0
  181. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_formats.py +0 -0
  182. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_git_action.py +0 -0
  183. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_github_fetch.py +0 -0
  184. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_github_ingest.py +0 -0
  185. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_graph_recon.py +0 -0
  186. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_graphify_build.py +0 -0
  187. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_graphify_hook_install.py +0 -0
  188. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_health.py +0 -0
  189. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_intake_job_outcome.py +0 -0
  190. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_intake_plan_review.py +0 -0
  191. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_journal.py +0 -0
  192. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_local_exec_provider.py +0 -0
  193. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_mcp_route_contract.py +0 -0
  194. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_memory_guard.py +0 -0
  195. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_phase_timer.py +0 -0
  196. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_plan_new_command.py +0 -0
  197. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_plan_run_activity.py +0 -0
  198. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_pr_identity.py +0 -0
  199. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_prd_artifact.py +0 -0
  200. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_project_path_resolution.py +0 -0
  201. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_proof_readers.py +0 -0
  202. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_providers.py +0 -0
  203. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_recon_session_isolation.py +0 -0
  204. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_resume.py +0 -0
  205. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_router.py +0 -0
  206. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_run_auto_agent_only.py +0 -0
  207. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_run_outputs.py +0 -0
  208. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_run_verdict_is_honest.py +0 -0
  209. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_schedule_api.py +0 -0
  210. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_schedule_outputs_and_plans.py +0 -0
  211. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_schedule_tickets.py +0 -0
  212. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_scheduler.py +0 -0
  213. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_scheduler_transient_retry.py +0 -0
  214. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_self_report.py +0 -0
  215. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_service_token_warning.py +0 -0
  216. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_stale_panel_restart.py +0 -0
  217. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_swagger_command.py +0 -0
  218. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_task_file_and_plan_run.py +0 -0
  219. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_task_file_format_doc.py +0 -0
  220. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_ticket_cli.py +0 -0
  221. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_ticket_fields.py +0 -0
  222. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_ticket_run_resolve_by_id.py +0 -0
  223. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_ticket_speed_fixes.py +0 -0
  224. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_tier_flag_parity.py +0 -0
  225. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_ui_gate.py +0 -0
  226. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_ui_lifespan.py +0 -0
  227. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_update_timer.py +0 -0
  228. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_updater_extras.py +0 -0
  229. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_updater_pep668.py +0 -0
  230. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_updater_reports_the_truth.py +0 -0
  231. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_url_access.py +0 -0
  232. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_vps_health.py +0 -0
  233. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_vps_only_reporting.py +0 -0
  234. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_windows_loop_identity.py +0 -0
  235. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/tests/test_windows_task_paths.py +0 -0
  236. {devops_bot_sdk-1.6.70 → devops_bot_sdk-1.6.73}/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.70
3
+ Version: 1.6.73
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.70
3
+ Version: 1.6.73
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
@@ -144,6 +144,7 @@ sdk/ui/static/tickets.html
144
144
  sdk/vendor/aidraftsman-cli-0.1.0.tgz
145
145
  tests/test_activity_collectors.py
146
146
  tests/test_activity_core.py
147
+ tests/test_activity_live_tracking.py
147
148
  tests/test_activity_macos.py
148
149
  tests/test_activity_os_setup.py
149
150
  tests/test_activity_pipeline.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.70
3
+ Version: 1.6.73
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.70" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.73" # 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
@@ -9,6 +9,14 @@ docs/ACTIVITY_AUDIT.md). Events used:
9
9
  4624 logon → LOGIN event + the interactive session (types 2/10/11)
10
10
  4634 / 4647 logoff → LOGOUT event, session closed
11
11
  4625 failed logon → LOGIN event, action ``login.failed``
12
+ 4778 RDP session reconnected → LOGIN event ``rdp.reconnect`` + the RDP session
13
+ 4779 RDP session disconnected→ LOGOUT event ``rdp.disconnect``, session closed
14
+
15
+ 4778/4779 matter more than 4624 in practice: people leave their remote desktop
16
+ session open and reconnect to it, which writes NO new 4624 type-10 logon. Without
17
+ them a VPS used all day showed no session at all, and every command its person ran
18
+ had no session to inherit — the fleet recorded 6 RDP sessions in two days.
19
+ They need the "Other Logon/Logoff Events" audit subcategory (``os_setup``).
12
20
 
13
21
  The event parser and the processor are pure; only :class:`WindowsEventLogCollector`
14
22
  touches the OS. Processes created by SYSTEM / LOCAL SERVICE / NETWORK SERVICE are
@@ -50,7 +58,7 @@ from sdk.activity.models import (
50
58
  logger = logging.getLogger("sdk.activity.collectors.windows")
51
59
 
52
60
  _NS = "{http://schemas.microsoft.com/win/2004/08/events/event}"
53
- EVENT_IDS = (4688, 4624, 4634, 4647, 4625)
61
+ EVENT_IDS = (4688, 4624, 4634, 4647, 4625, 4778, 4779)
54
62
  _MAX_PID_CACHE = 20_000
55
63
  #: Executables the collector itself spawns to poll the log.
56
64
  _OWN_TOOLS = ("wevtutil.exe",)
@@ -117,6 +125,11 @@ def _hex(value: str | None) -> int:
117
125
  return 0
118
126
 
119
127
 
128
+ def _remote(ip: str) -> str | None:
129
+ """A client address worth recording; ``None`` for blanks and loopback."""
130
+ return ip if ip and ip not in ("-", "::1", "127.0.0.1", "LOCAL") else None
131
+
132
+
120
133
  def _is_person(user: str) -> bool:
121
134
  upper = user.upper()
122
135
  return bool(user) and not user.endswith("$") and upper not in _NON_PERSON_USERS \
@@ -153,6 +166,10 @@ class WindowsEventProcessor:
153
166
  self._logoff(ev)
154
167
  elif ev.event_id == 4625:
155
168
  self._failed_logon(ev)
169
+ elif ev.event_id == 4778:
170
+ self._rdp_reconnect(ev)
171
+ elif ev.event_id == 4779:
172
+ self._rdp_disconnect(ev)
156
173
  except Exception as exc: # noqa: BLE001 — one bad record must not stop the poll
157
174
  logger.warning("eventlog: could not process %s/%s: %s", ev.event_id, ev.record_id, exc)
158
175
 
@@ -170,22 +187,65 @@ class WindowsEventProcessor:
170
187
  if kind is None or not _is_person(user):
171
188
  return
172
189
  logon_id = d.get("TargetLogonId", "").lower()
173
- ip = d.get("IpAddress", "")
174
- remote = ip if ip and ip not in ("-", "::1", "127.0.0.1") else None
190
+ remote = _remote(d.get("IpAddress", ""))
191
+ key, run_id = self._open_session(logon_id, user, kind, remote, ev.time)
192
+ recorder.record_event(ActivityEvent(
193
+ os=Os.WINDOWS, source=Source.WIN_EVENTLOG, category=Category.LOGIN, action="login",
194
+ occurredAt=ev.time, sessionKey=key, osUser=user, target=remote,
195
+ agentRunId=run_id, metadata={"logonType": logon_type},
196
+ ))
197
+
198
+ def _open_session(
199
+ self, logon_id: str, user: str, kind: SessionKind, remote: str | None, when: float,
200
+ ) -> tuple[str, str | None]:
201
+ """Start a person's session for ``logon_id``; returns (session key, agent run id)."""
175
202
  verdict = classify(ProcessFacts(
176
203
  login_kind=kind, has_tty=True, is_agent_os_user=self._is_agent_user(user),
177
204
  ))
178
- key = f"win:{logon_id}:{int(ev.time)}"
205
+ key = f"win:{logon_id}:{int(when)}"
179
206
  info = self._book.ensure(
180
- key, started=ev.time, verdict=verdict, kind=kind, os_user=user, remote_ip=remote,
207
+ key, started=when, verdict=verdict, kind=kind, os_user=user, remote_ip=remote,
181
208
  )
182
209
  self._logons[logon_id] = key
183
210
  self._kinds[logon_id] = kind
211
+ return key, info.agent_run_id
212
+
213
+ def _rdp_reconnect(self, ev: WinEvent) -> None:
214
+ """4778: someone came back to an existing desktop session — they are here now.
215
+
216
+ The logon id is the ORIGINAL logon's, so processes the person starts from here
217
+ on carry it as their ``SubjectLogonId`` and inherit this session.
218
+ """
219
+ d = ev.data
220
+ user = d.get("AccountName", "")
221
+ logon_id = d.get("LogonID", "").lower()
222
+ if not logon_id or not _is_person(user):
223
+ return
224
+ remote = _remote(d.get("ClientAddress", ""))
225
+ key = self._logons.get(logon_id)
226
+ run_id = None
227
+ if key is None:
228
+ key, run_id = self._open_session(logon_id, user, SessionKind.RDP, remote, ev.time)
184
229
  recorder.record_event(ActivityEvent(
185
- os=Os.WINDOWS, source=Source.WIN_EVENTLOG, category=Category.LOGIN, action="login",
186
- occurredAt=ev.time, sessionKey=key, osUser=user, target=remote,
187
- agentRunId=info.agent_run_id, metadata={"logonType": logon_type},
230
+ os=Os.WINDOWS, source=Source.WIN_EVENTLOG, category=Category.LOGIN,
231
+ action="rdp.reconnect", occurredAt=ev.time, sessionKey=key, osUser=user,
232
+ target=remote, agentRunId=run_id,
233
+ metadata={"clientName": d.get("ClientName") or None},
234
+ ))
235
+
236
+ def _rdp_disconnect(self, ev: WinEvent) -> None:
237
+ """4779: the person left the desktop; the logon itself lives on until 4634."""
238
+ logon_id = ev.data.get("LogonID", "").lower()
239
+ key = self._logons.pop(logon_id, None)
240
+ self._kinds.pop(logon_id, None)
241
+ if not key:
242
+ return
243
+ recorder.record_event(ActivityEvent(
244
+ os=Os.WINDOWS, source=Source.WIN_EVENTLOG, category=Category.LOGOUT,
245
+ action="rdp.disconnect", occurredAt=ev.time, sessionKey=key,
246
+ osUser=ev.data.get("AccountName") or None,
188
247
  ))
248
+ self._book.end(key, ev.time)
189
249
 
190
250
  def _logoff(self, ev: WinEvent) -> None:
191
251
  logon_id = ev.data.get("TargetLogonId", "").lower()
@@ -79,6 +79,9 @@ _WIN_SUBCATEGORIES = (
79
79
  ("Process Creation", ("/success:enable",)),
80
80
  ("Logon", ("/success:enable", "/failure:enable")),
81
81
  ("Logoff", ("/success:enable",)),
82
+ # 4778/4779 — RDP reconnect / disconnect. Most people reconnect to an open
83
+ # desktop rather than log on afresh, so without this nobody is ever "there".
84
+ ("Other Logon/Logoff Events", ("/success:enable",)),
82
85
  )
83
86
  _WIN_CMDLINE_KEY = r"HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\System\Audit"
84
87
  _WIN_CMDLINE_VALUE = "ProcessCreationIncludeCmdLine_Enabled"
@@ -87,6 +90,7 @@ _WIN_MANUAL = (
87
90
  'auditpol /set /subcategory:"Process Creation" /success:enable\n'
88
91
  'auditpol /set /subcategory:"Logon" /success:enable /failure:enable\n'
89
92
  'auditpol /set /subcategory:"Logoff" /success:enable\n'
93
+ 'auditpol /set /subcategory:"Other Logon/Logoff Events" /success:enable\n'
90
94
  f"reg add {_WIN_CMDLINE_KEY} /v {_WIN_CMDLINE_VALUE} /t REG_DWORD /d 1 /f\n"
91
95
  f"wevtutil sl Security /ms:{_WIN_LOG_BYTES}\n"
92
96
  f"then add {AUDIT_FLAG}=1 to %USERPROFILE%\\.agentos\\.env and restart the panel"
@@ -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
@@ -188,17 +190,42 @@ def discard_spool(spool: Spool | None = None) -> int:
188
190
  spool.ack(chunk)
189
191
 
190
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
+
191
209
  async def ship_tick(
192
210
  is_vps: Callable[[], Awaitable[bool | None]] | None = None,
193
211
  ) -> FlushResult | None:
194
- """One cron tick: ship only from the caller's assigned VPS.
212
+ """One cron tick: ship only from the caller's assigned VPS, newest first.
195
213
 
196
214
  The relay files every batch under the TOKEN's VPS, so a laptop running the SDK
197
215
  with the same token put its own events in the VPS's audit trail. On a machine
198
216
  that is definitely not the VPS the queue is discarded — kept, it would only grow
199
217
  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.
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.
201
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)
202
229
  if is_vps is None:
203
230
  from sdk import vps_identity
204
231
  is_vps = vps_identity.is_assigned_vps
@@ -222,7 +249,7 @@ async def ship_tick(
222
249
 
223
250
 
224
251
  async def run_forever(interval_s: float | None = None, *, initial_delay_s: float = 30.0) -> None:
225
- """Flush every ``interval_s`` (default 5 minutes) until cancelled."""
252
+ """Flush every ``interval_s`` (default 60 s) until cancelled."""
226
253
  await asyncio.sleep(initial_delay_s)
227
254
  while True:
228
255
  try:
@@ -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:
@@ -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 ───────────────────────────────
@@ -500,6 +507,58 @@ class TestWindowsProcessing:
500
507
  assert (events[0]["action"], events[0]["severity"], events[0]["target"]) == (
501
508
  "login.failed", "WARNING", "198.51.100.9")
502
509
 
510
+ RECONNECT = dict(AccountName="alice", AccountDomain="WIN-HOST", LogonID="0xabc123",
511
+ SessionName="RDP-Tcp#3", ClientName="ALICE-LAPTOP",
512
+ ClientAddress="203.0.113.7")
513
+
514
+ def test_an_rdp_reconnect_is_a_human_session_its_commands_inherit(self):
515
+ """People reconnect to an open desktop; no fresh 4624 is written for it."""
516
+ _win(
517
+ _xml(4778, 1, **self.RECONNECT),
518
+ _xml(4688, 2, NewProcessId="0x200", ProcessId="0x100", SubjectUserName="alice",
519
+ SubjectLogonId="0xabc123", NewProcessName="C:\\x\\chrome.exe"),
520
+ )
521
+ sessions, events = _queued()
522
+ s = sessions[0]
523
+ assert s["externalKey"].startswith("win:0xabc123:")
524
+ assert (s["kind"], s["actorType"], s["remoteIp"], s["osUser"]) == (
525
+ "RDP", "HUMAN", "203.0.113.7", "alice")
526
+ login, cmd = events
527
+ assert (login["category"], login["action"], login["sessionKey"]) == (
528
+ "LOGIN", "rdp.reconnect", s["externalKey"])
529
+ assert login["metadata"] == {"clientName": "ALICE-LAPTOP"}
530
+ assert (cmd["sessionKey"], cmd["actorType"]) == (s["externalKey"], "HUMAN")
531
+
532
+ def test_an_rdp_disconnect_closes_the_session(self):
533
+ _win(_xml(4778, 1, **self.RECONNECT),
534
+ _xml(4779, 2, **self.RECONNECT))
535
+ sessions, events = _queued()
536
+ assert "endedAt" in sessions[0]
537
+ assert (events[-1]["category"], events[-1]["action"]) == ("LOGOUT", "rdp.disconnect")
538
+
539
+ def test_a_reconnect_inside_a_session_we_already_track_reuses_it(self):
540
+ _win(_xml(4624, 1, **self.LOGON),
541
+ _xml(4778, 2, **self.RECONNECT))
542
+ sessions, events = _queued()
543
+ assert len(sessions) == 1
544
+ assert {e["sessionKey"] for e in events} == {sessions[0]["externalKey"]}
545
+
546
+ def test_disconnect_then_reconnect_opens_a_new_session(self):
547
+ _win(_xml(4778, 1, **self.RECONNECT),
548
+ _xml(4779, 2, time="2026-10-09T12:10:00Z", **self.RECONNECT),
549
+ _xml(4778, 3, time="2026-10-09T13:00:00Z", **self.RECONNECT))
550
+ sessions, _ = _queued()
551
+ assert len({s["externalKey"] for s in sessions}) == 2
552
+
553
+ @pytest.mark.parametrize("user", ["SYSTEM", "WIN-HOST$", "DWM-2", ""])
554
+ def test_a_reconnect_by_a_non_person_is_ignored(self, user):
555
+ _win(_xml(4778, 1, **{**self.RECONNECT, "AccountName": user}))
556
+ assert _queued() == ([], [])
557
+
558
+ def test_a_disconnect_for_an_unknown_logon_is_ignored(self):
559
+ _win(_xml(4779, 1, **self.RECONNECT))
560
+ assert _queued() == ([], [])
561
+
503
562
  def test_a_secret_on_the_command_line_is_redacted(self):
504
563
  _win(_xml(4624, 1, **self.LOGON),
505
564
  _xml(4688, 2, NewProcessId="0x200", ProcessId="0x100", SubjectUserName="alice",
@@ -0,0 +1,145 @@
1
+ """Live tracking: the newest activity ships first, and stale backlog is discarded.
2
+
3
+ The incident: hosts that had been offline, or flooded by a brute-forced RDP port,
4
+ shipped their spool oldest-first. At 10:00 several VPS were delivering events from
5
+ two and three days earlier, so the dashboard was never live.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import time
11
+ from datetime import datetime, timezone
12
+
13
+ import pytest
14
+
15
+ from sdk.activity import settings, shipper
16
+ from sdk.activity.spool import Spool
17
+
18
+ NOW = datetime(2026, 10, 22, 10, 0, 0, tzinfo=timezone.utc).timestamp()
19
+
20
+
21
+ def _iso(minutes_ago: float) -> str:
22
+ t = datetime.fromtimestamp(NOW - minutes_ago * 60, timezone.utc)
23
+ return t.isoformat(timespec="milliseconds").replace("+00:00", "Z")
24
+
25
+
26
+ def _ev(eid: str, minutes_ago: float) -> dict:
27
+ return {"eventId": eid, "occurredAt": _iso(minutes_ago), "os": "LINUX", "source": "AUDITD",
28
+ "category": "COMMAND", "action": "exec", "actorType": "HUMAN", "actorConfidence": 90}
29
+
30
+
31
+ def _sess(key: str, started_ago: float, ended_ago: float | None = None) -> dict:
32
+ s = {"externalKey": key, "kind": "SSH", "actorType": "HUMAN", "actorConfidence": 90,
33
+ "startedAt": _iso(started_ago)}
34
+ if ended_ago is not None:
35
+ s["endedAt"] = _iso(ended_ago)
36
+ return s
37
+
38
+
39
+ @pytest.fixture
40
+ def spool(tmp_path):
41
+ s = Spool(tmp_path / "live.db")
42
+ yield s
43
+ s.close()
44
+
45
+
46
+ @pytest.fixture(autouse=True)
47
+ def hermetic(monkeypatch):
48
+ import sdk.config as cfg
49
+ monkeypatch.setattr(cfg, "_dotenv_value", lambda key: "")
50
+ monkeypatch.delenv("AGENTOS_ACTIVITY_MAX_AGE_S", raising=False)
51
+
52
+
53
+ class TestStaleBacklogIsDiscarded:
54
+ def test_events_older_than_an_hour_are_dropped(self, spool):
55
+ spool.add_event(_ev("days-old", 3 * 24 * 60))
56
+ spool.add_event(_ev("61-min", 61))
57
+ spool.add_event(_ev("59-min", 59))
58
+ spool.add_event(_ev("now", 0))
59
+ assert shipper.purge_stale_rows(spool, now=NOW) == 2
60
+ assert sorted(e["eventId"] for e in spool.next_chunk().events) == ["59-min", "now"]
61
+
62
+ def test_an_open_session_is_kept_however_old(self, spool):
63
+ spool.add_session(_sess("still-logged-in", started_ago=600))
64
+ spool.add_session(_sess("ended-long-ago", started_ago=600, ended_ago=300))
65
+ spool.add_session(_sess("ended-just-now", started_ago=600, ended_ago=5))
66
+ assert shipper.purge_stale_rows(spool, now=NOW) == 1
67
+ assert sorted(s["externalKey"] for s in spool.next_chunk().sessions) == [
68
+ "ended-just-now", "still-logged-in"]
69
+
70
+ def test_the_cut_off_is_configurable(self, spool, monkeypatch):
71
+ monkeypatch.setenv("AGENTOS_ACTIVITY_MAX_AGE_S", "86400")
72
+ spool.add_event(_ev("3h", 180))
73
+ spool.add_event(_ev("2d", 2 * 24 * 60))
74
+ assert shipper.purge_stale_rows(spool, now=NOW) == 1
75
+ assert [e["eventId"] for e in spool.next_chunk().events] == ["3h"]
76
+
77
+ def test_zero_keeps_everything(self, spool, monkeypatch):
78
+ monkeypatch.setenv("AGENTOS_ACTIVITY_MAX_AGE_S", "0")
79
+ spool.add_event(_ev("ancient", 10 * 24 * 60))
80
+ assert shipper.purge_stale_rows(spool, now=NOW) == 0
81
+ assert spool.count() == 1
82
+
83
+ def test_python_fallback_matches_json1(self, spool, monkeypatch):
84
+ """SQLite without JSON1 filters in Python, to the same result."""
85
+ import sqlite3
86
+
87
+ spool.add_event(_ev("old", 120))
88
+ spool.add_event(_ev("new", 1))
89
+ real = spool._db
90
+
91
+ class NoJson:
92
+ def execute(self, sql, *a):
93
+ if "json_extract" in sql:
94
+ raise sqlite3.OperationalError("no such function: json_extract")
95
+ return real.execute(sql, *a)
96
+
97
+ def executemany(self, sql, *a):
98
+ return real.executemany(sql, *a)
99
+ monkeypatch.setattr(spool, "_db", NoJson())
100
+ assert shipper.purge_stale_rows(spool, now=NOW) == 1
101
+ monkeypatch.setattr(spool, "_db", real)
102
+ assert [e["eventId"] for e in spool.next_chunk().events] == ["new"]
103
+
104
+
105
+ class TestNewestFirst:
106
+ def test_a_backlog_never_holds_back_the_current_events(self, spool):
107
+ for i in range(1200): # a flood, oldest first
108
+ spool.add_event(_ev(f"old{i}", 50 - i * 0.01))
109
+ spool.add_event(_ev("current", 0))
110
+ first = spool.next_chunk(max_events=500)
111
+ assert first.events[0]["eventId"] == "current"
112
+
113
+
114
+ class TestShipTickPurgesFirst:
115
+ def test_stale_rows_are_gone_before_anything_ships(self, tmp_path, monkeypatch):
116
+ s = Spool(tmp_path / "tick.db")
117
+ s.add_event(_ev("stale", 120))
118
+ s.add_event(_ev("fresh", 1))
119
+ monkeypatch.setattr(settings, "get_spool", lambda: s)
120
+ monkeypatch.setattr(time, "time", lambda: NOW)
121
+ seen = []
122
+
123
+ async def fake_flush():
124
+ seen.extend(e["eventId"] for e in s.next_chunk().events)
125
+ return shipper.FlushResult()
126
+ monkeypatch.setattr(shipper, "flush_once", fake_flush)
127
+
128
+ async def yes():
129
+ return True
130
+ asyncio.run(shipper.ship_tick(yes))
131
+ s.close()
132
+ assert seen == ["fresh"]
133
+
134
+ def test_purge_also_runs_when_vps_identity_is_unknown(self, tmp_path, monkeypatch):
135
+ s = Spool(tmp_path / "tick2.db")
136
+ s.add_event(_ev("stale", 120))
137
+ monkeypatch.setattr(settings, "get_spool", lambda: s)
138
+ monkeypatch.setattr(time, "time", lambda: NOW)
139
+
140
+ async def unknown():
141
+ return None
142
+ asyncio.run(shipper.ship_tick(unknown))
143
+ n = s.count()
144
+ s.close()
145
+ assert n == 0
@@ -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
  USERS = {501: "alice", 502: "agentos", 0: "root"}