devops-bot-sdk 1.6.44__tar.gz → 1.6.47__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 (222) hide show
  1. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/__init__.py +2 -2
  4. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/collectors/__init__.py +38 -2
  5. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/collectors/linux_auditd.py +11 -1
  6. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/collectors/windows_eventlog.py +28 -1
  7. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/os_setup.py +193 -7
  8. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/service.py +87 -3
  9. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_activity_collectors.py +121 -0
  10. devops_bot_sdk-1.6.47/tests/test_activity_os_setup.py +283 -0
  11. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_update_timer.py +94 -0
  12. devops_bot_sdk-1.6.44/tests/test_activity_os_setup.py +0 -151
  13. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/README.md +0 -0
  14. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  15. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  16. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  17. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/requires.txt +0 -0
  18. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  19. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/pyproject.toml +0 -0
  20. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/__init__.py +0 -0
  21. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/classifier.py +0 -0
  22. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/collectors/common.py +0 -0
  23. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/collectors/macos_eslogger.py +0 -0
  24. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/models.py +0 -0
  25. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/recorder.py +0 -0
  26. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/redact.py +0 -0
  27. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/settings.py +0 -0
  28. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/shipper.py +0 -0
  29. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/activity/spool.py +0 -0
  30. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agentd.py +0 -0
  31. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/__init__.py +0 -0
  32. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/activity.py +0 -0
  33. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/builder.py +0 -0
  34. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/chat.py +0 -0
  35. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/connectors.py +0 -0
  36. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/__init__.py +0 -0
  37. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/claude_code.py +0 -0
  38. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/http.py +0 -0
  39. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/mcp.py +0 -0
  40. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/n8n.py +0 -0
  41. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/engines/shell.py +0 -0
  42. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/integrations.py +0 -0
  43. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/inventory.py +0 -0
  44. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/library/__init__.py +0 -0
  45. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/manifest.py +0 -0
  46. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/prompt_studio.py +0 -0
  47. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/prompt_template.py +0 -0
  48. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/prompt_vars.py +0 -0
  49. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/run_log.py +0 -0
  50. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/schedules.py +0 -0
  51. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/secrets.py +0 -0
  52. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/ticket_run.py +0 -0
  53. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/usage.py +0 -0
  54. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/agents/varstore.py +0 -0
  55. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/browser_profile.py +0 -0
  56. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/cli.py +0 -0
  57. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/client.py +0 -0
  58. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/collectors/__init__.py +0 -0
  59. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/collectors/files.py +0 -0
  60. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/collectors/process.py +0 -0
  61. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/collectors/screenshot.py +0 -0
  62. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/config.py +0 -0
  63. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/crucial.py +0 -0
  64. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/departments.py +0 -0
  65. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/__init__.py +0 -0
  66. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/assets.py +0 -0
  67. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/bootstrap.py +0 -0
  68. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/browser.py +0 -0
  69. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/compare.py +0 -0
  70. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/design_verify/loop.py +0 -0
  71. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/estimator.py +0 -0
  72. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/exceptions.py +0 -0
  73. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/firewall.py +0 -0
  74. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/formats.py +0 -0
  75. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/git_ops.py +0 -0
  76. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/github_fetch.py +0 -0
  77. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/github_ingest.py +0 -0
  78. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/graph_recon.py +0 -0
  79. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/graphify.py +0 -0
  80. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/hooks/__init__.py +0 -0
  81. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/hooks/crucial_guard.py +0 -0
  82. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/intake.py +0 -0
  83. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ipc/__init__.py +0 -0
  84. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ipc/electron_bridge.py +0 -0
  85. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ipc/handlers.py +0 -0
  86. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/journal.py +0 -0
  87. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/local_exec.py +0 -0
  88. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/memory_guard.py +0 -0
  89. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/models/__init__.py +0 -0
  90. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/models/envelope.py +0 -0
  91. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/models/requests.py +0 -0
  92. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/models/responses.py +0 -0
  93. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/models/snapshots.py +0 -0
  94. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/phase_timer.py +0 -0
  95. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/plan_run.py +0 -0
  96. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/platform_compat.py +0 -0
  97. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/prd_progress.py +0 -0
  98. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/proof_readers.py +0 -0
  99. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/providers.py +0 -0
  100. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/py.typed +0 -0
  101. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/resume.py +0 -0
  102. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/router.py +0 -0
  103. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/run_auto.py +0 -0
  104. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/scheduler.py +0 -0
  105. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/self_report.py +0 -0
  106. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/self_report_local.py +0 -0
  107. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/sse.py +0 -0
  108. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/swagger.py +0 -0
  109. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/task_file.py +0 -0
  110. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/task_types.py +0 -0
  111. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/test.py +0 -0
  112. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/test_pipeline.py +0 -0
  113. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ticket_fields.py +0 -0
  114. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/tiers/__init__.py +0 -0
  115. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/tiers/mini_agent.py +0 -0
  116. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/__init__.py +0 -0
  117. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/api.py +0 -0
  118. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/dashboard.py +0 -0
  119. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/health.py +0 -0
  120. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/ip_allowlist.py +0 -0
  121. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/jobs.py +0 -0
  122. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/models.py +0 -0
  123. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/runner.py +0 -0
  124. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/security.py +0 -0
  125. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/server.py +0 -0
  126. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/session.py +0 -0
  127. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/setup_manager.py +0 -0
  128. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/agents.html +0 -0
  129. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/app.js +0 -0
  130. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/chat.html +0 -0
  131. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/custom-agents.html +0 -0
  132. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/default-agents.html +0 -0
  133. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/index.html +0 -0
  134. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/report.html +0 -0
  135. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/runs.html +0 -0
  136. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/schedules.html +0 -0
  137. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/setup.html +0 -0
  138. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/styles.css +0 -0
  139. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/static/tickets.html +0 -0
  140. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/ui/swagger.py +0 -0
  141. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/updater.py +0 -0
  142. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/url_access.py +0 -0
  143. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/vps_health.py +0 -0
  144. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/sdk/write_hook.py +0 -0
  145. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/setup.cfg +0 -0
  146. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_activity_core.py +0 -0
  147. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_activity_macos.py +0 -0
  148. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_activity_pipeline.py +0 -0
  149. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_activity.py +0 -0
  150. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_activity_tracking.py +0 -0
  151. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_chat_features.py +0 -0
  152. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_listing.py +0 -0
  153. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_prompt_async.py +0 -0
  154. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_prompt_models.py +0 -0
  155. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_run_ticket.py +0 -0
  156. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_schedules.py +0 -0
  157. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agent_workspace_settings.py +0 -0
  158. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_agents_connectors_builder.py +0 -0
  159. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_allowed_tools_config.py +0 -0
  160. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_auto_agent_dispatch.py +0 -0
  161. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_browser_profile.py +0 -0
  162. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_cheap_tier.py +0 -0
  163. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_client_jira_projects.py +0 -0
  164. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_config_token_diagnostics.py +0 -0
  165. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_configure_token_owner.py +0 -0
  166. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_console_encoding.py +0 -0
  167. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_crash_recovery.py +0 -0
  168. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_departments.py +0 -0
  169. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_done_transition.py +0 -0
  170. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_epic_decompose_run.py +0 -0
  171. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_epic_inherited_agent_id.py +0 -0
  172. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_epic_session_isolation.py +0 -0
  173. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_estimator.py +0 -0
  174. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_firewall_open_on_update.py +0 -0
  175. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_formats.py +0 -0
  176. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_git_action.py +0 -0
  177. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_github_fetch.py +0 -0
  178. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_github_ingest.py +0 -0
  179. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_graph_recon.py +0 -0
  180. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_graphify_build.py +0 -0
  181. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_graphify_hook_install.py +0 -0
  182. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_health.py +0 -0
  183. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_intake_job_outcome.py +0 -0
  184. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_intake_plan_review.py +0 -0
  185. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_journal.py +0 -0
  186. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_local_exec_provider.py +0 -0
  187. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_mcp_route_contract.py +0 -0
  188. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_memory_guard.py +0 -0
  189. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_phase_timer.py +0 -0
  190. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_plan_new_command.py +0 -0
  191. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_plan_run_activity.py +0 -0
  192. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_pr_identity.py +0 -0
  193. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_prd_artifact.py +0 -0
  194. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_project_path_resolution.py +0 -0
  195. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_proof_readers.py +0 -0
  196. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_providers.py +0 -0
  197. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_recon_session_isolation.py +0 -0
  198. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_resume.py +0 -0
  199. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_router.py +0 -0
  200. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_run_auto_agent_only.py +0 -0
  201. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_run_outputs.py +0 -0
  202. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_run_verdict_is_honest.py +0 -0
  203. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_schedule_api.py +0 -0
  204. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_schedule_outputs_and_plans.py +0 -0
  205. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_scheduler.py +0 -0
  206. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_self_report.py +0 -0
  207. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_service_token_warning.py +0 -0
  208. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_swagger_command.py +0 -0
  209. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_task_file_and_plan_run.py +0 -0
  210. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_task_file_format_doc.py +0 -0
  211. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_ticket_fields.py +0 -0
  212. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_ticket_run_resolve_by_id.py +0 -0
  213. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_ticket_speed_fixes.py +0 -0
  214. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_tier_flag_parity.py +0 -0
  215. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_ui_gate.py +0 -0
  216. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_ui_lifespan.py +0 -0
  217. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_updater_extras.py +0 -0
  218. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_updater_reports_the_truth.py +0 -0
  219. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_url_access.py +0 -0
  220. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_vps_health.py +0 -0
  221. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/tests/test_windows_loop_identity.py +0 -0
  222. {devops_bot_sdk-1.6.44 → devops_bot_sdk-1.6.47}/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.44
3
+ Version: 1.6.47
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.44
3
+ Version: 1.6.47
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
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.44
3
+ Version: 1.6.47
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.44" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.47" # 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
@@ -1,4 +1,4 @@
1
- """OS-level collectors — opt-in (``AGENTOS_ACTIVITY_AUDIT=1``).
1
+ """OS-level collectors (``AGENTOS_ACTIVITY_AUDIT=1``, switched on by ``os_setup``).
2
2
 
3
3
  Linux auditd sdk.activity.collectors.linux_auditd
4
4
  Windows Security log sdk.activity.collectors.windows_eventlog
@@ -70,9 +70,45 @@ async def run_forever(collector: Any) -> None:
70
70
  logger.exception("activity collector crashed")
71
71
 
72
72
 
73
+ async def _setup_then_run() -> None:
74
+ """Turn the OS audit on (``os_setup.ensure``), then run the collector — no restart.
75
+
76
+ The panel is the one process that is always running, so it is where a box whose
77
+ install/update ran unprivileged gets set up: as root/elevated directly, or through
78
+ passwordless ``sudo`` (Linux). In a thread, because a first run may install auditd.
79
+ """
80
+ from sdk.activity import os_setup
81
+
82
+ try:
83
+ _, msg = await asyncio.to_thread(os_setup.ensure)
84
+ except Exception as exc: # noqa: BLE001 — ensure never raises, but be certain
85
+ logger.warning("activity audit setup crashed: %s", exc)
86
+ return
87
+ if msg:
88
+ logger.warning("%s", msg)
89
+ if not settings.audit_enabled():
90
+ return
91
+ try:
92
+ collector = make_collector()
93
+ except Exception as exc: # noqa: BLE001
94
+ logger.warning("activity collector could not be created: %s", exc)
95
+ return
96
+ if collector is not None:
97
+ logger.info("activity collector starting (%s)", type(collector).__name__)
98
+ await run_forever(collector)
99
+
100
+
73
101
  def start() -> asyncio.Task[None] | None:
74
- if not (settings.enabled() and settings.audit_enabled()):
102
+ if not settings.enabled():
75
103
  return None
104
+ if not settings.audit_enabled():
105
+ from sdk.activity import os_setup
106
+
107
+ # An explicit AGENTOS_ACTIVITY_AUDIT=0 / AGENTOS_NO_AUDIT_SETUP=1 is the
108
+ # operator's decision: respected, nothing touched.
109
+ if os_setup._opted_out():
110
+ return None
111
+ return asyncio.ensure_future(_setup_then_run())
76
112
  try:
77
113
  collector = make_collector()
78
114
  except Exception as exc: # noqa: BLE001
@@ -159,6 +159,11 @@ class AuditdParser:
159
159
  inner = re.search(r"msg='([^']*)'", body)
160
160
  if inner:
161
161
  rec.update({f"m_{k}": v for k, v in _kv(inner.group(1)).items()})
162
+ # acct is quoted when printable and hex-encoded otherwise; as with EXECVE
163
+ # args, only the raw match says which ("dead" is a name AND valid hex).
164
+ for key, raw in _KV.findall(inner.group(1)):
165
+ if key == "acct":
166
+ rec["m_acct"] = _unquote(raw) if raw[:1] in "\"'" else _unhex(raw)
162
167
  if rtype == "EXECVE":
163
168
  # Quoting is the ONLY thing that tells a literal argument from a hex-encoded
164
169
  # one ("beef" is valid hex), so read it from the raw match, not from _kv.
@@ -217,9 +222,14 @@ class AuditdParser:
217
222
  terminal = rec.get("m_terminal", "")
218
223
 
219
224
  if not ok:
225
+ # The attempted account name: a failed login has no uid to resolve (it is
226
+ # unset, or an invalid user), so without acct the record says nothing about
227
+ # which names an attacker is trying.
228
+ acct = rec.get("m_acct", "")
229
+ tried = acct if acct and acct not in ("(unknown)", "?") else user
220
230
  recorder.record_event(ActivityEvent(
221
231
  os=Os.LINUX, source=Source.AUDITD, category=Category.LOGIN, action="login.failed",
222
- occurredAt=ts, osUser=user, target=remote, severity=Severity.WARNING,
232
+ occurredAt=ts, osUser=tried, target=remote, severity=Severity.WARNING,
223
233
  actorType=ActorType.UNKNOWN, actorConfidence=0,
224
234
  ))
225
235
  return
@@ -14,6 +14,11 @@ The event parser and the processor are pure; only :class:`WindowsEventLogCollect
14
14
  touches the OS. Processes created by SYSTEM / LOCAL SERVICE / NETWORK SERVICE are
15
15
  skipped by default (``ignore_system``): they are thousands of events an hour of
16
16
  Windows housekeeping that no human or agent did.
17
+
18
+ The collector's own polling is skipped too: every poll runs ``wevtutil.exe`` from the
19
+ panel process (as the panel's user, not SYSTEM), and each of those is itself a 4688.
20
+ Unfiltered, that is ~8,600 "Administrator ran wevtutil" events a day per VPS, each
21
+ poll recording the previous one.
17
22
  """
18
23
  from __future__ import annotations
19
24
 
@@ -47,6 +52,9 @@ logger = logging.getLogger("sdk.activity.collectors.windows")
47
52
  _NS = "{http://schemas.microsoft.com/win/2004/08/events/event}"
48
53
  EVENT_IDS = (4688, 4624, 4634, 4647, 4625)
49
54
  _MAX_PID_CACHE = 20_000
55
+ #: Executables the collector itself spawns to poll the log.
56
+ _OWN_TOOLS = ("wevtutil.exe",)
57
+ _MAX_OWN_PIDS = 1_000
50
58
 
51
59
  #: Logon types that are a PERSON at a keyboard / remote desktop.
52
60
  _INTERACTIVE = {2: SessionKind.CONSOLE, 11: SessionKind.CONSOLE, 10: SessionKind.RDP}
@@ -123,11 +131,14 @@ class WindowsEventProcessor:
123
131
  registry: AgentProcessRegistry | None = None,
124
132
  book: SessionBook | None = None,
125
133
  ignore_system: bool = True,
134
+ self_pid: int | None = None,
126
135
  ) -> None:
127
136
  self._agent_user = agent_user.lower() if agent_user else None
128
137
  self._registry = registry or AgentProcessRegistry()
129
138
  self._book = book or SessionBook()
130
139
  self._ignore_system = ignore_system
140
+ self._self_pid = os.getpid() if self_pid is None else self_pid
141
+ self._own: OrderedDict[int, None] = OrderedDict() # pids of our own polls + children
131
142
  self._ppid: OrderedDict[int, int] = OrderedDict()
132
143
  self._logons: dict[str, str] = {} # logon id → session key
133
144
  self._kinds: dict[str, SessionKind] = {} # logon id → login kind
@@ -202,6 +213,9 @@ class WindowsEventProcessor:
202
213
  d = ev.data
203
214
  pid, ppid = _hex(d.get("NewProcessId")), _hex(d.get("ProcessId"))
204
215
  self._remember_ppid(pid, ppid)
216
+ image = d.get("NewProcessName", "")
217
+ if self._is_own_poll(pid, ppid, image):
218
+ return
205
219
  logon_id = d.get("SubjectLogonId", "").lower()
206
220
  if self._ignore_system and logon_id in _SYSTEM_LOGON_IDS:
207
221
  return
@@ -232,7 +246,6 @@ class WindowsEventProcessor:
232
246
  session_key, started=ev.time, verdict=verdict, os_user=user,
233
247
  ).agent_run_id
234
248
 
235
- image = d.get("NewProcessName", "")
236
249
  recorder.record_event(ActivityEvent(
237
250
  os=Os.WINDOWS, source=Source.WIN_EVENTLOG, category=Category.COMMAND, action="exec",
238
251
  occurredAt=ev.time, sessionKey=session_key,
@@ -243,6 +256,20 @@ class WindowsEventProcessor:
243
256
  metadata={"parent": d.get("ParentProcessName") or None},
244
257
  ))
245
258
 
259
+ def _is_own_poll(self, pid: int, ppid: int, image: str) -> bool:
260
+ """A ``wevtutil`` this process spawned to poll, or a child of one (its ``conhost``).
261
+
262
+ Matched by parent pid AND image name, so anything else the panel spawns —
263
+ an agent run, ``git``, ``claude`` — is still recorded.
264
+ """
265
+ name = image.rsplit("\\", 1)[-1].lower()
266
+ if not ((ppid == self._self_pid and name in _OWN_TOOLS) or ppid in self._own):
267
+ return False
268
+ self._own[pid] = None
269
+ while len(self._own) > _MAX_OWN_PIDS:
270
+ self._own.popitem(last=False)
271
+ return True
272
+
246
273
  def _remember_ppid(self, pid: int, ppid: int) -> None:
247
274
  self._ppid[pid] = ppid
248
275
  self._ppid.move_to_end(pid)
@@ -19,6 +19,17 @@ the same contract:
19
19
  log anyway.)
20
20
  * **never raises** — an update must not fail because of the audit trail.
21
21
 
22
+ **A panel that runs as an ordinary user** (a user systemd unit, the common case on
23
+ a VPS set up without ``sudo``) was the gap that left most Linux boxes unaudited even
24
+ when that user had root through ``sudo``. Three things now close it:
25
+
26
+ * unprivileged, :func:`ensure` runs its privileged half through ``sudo -n`` when
27
+ sudo needs no password, so nobody has to type a command;
28
+ * under ``sudo`` the flag goes into the INVOKING user's ``~/.agentos/.env``
29
+ (``SUDO_USER``), not ``/root`` — the panel never reads root's home;
30
+ * ``auditd``'s ``log_group`` is set to that user's private group, because
31
+ ``audit.log`` is root-only and a collector that cannot read it records nothing.
32
+
22
33
  Opt out with ``AGENTOS_NO_AUDIT_SETUP=1``. An operator who set
23
34
  ``AGENTOS_ACTIVITY_AUDIT=0`` (or ``AGENTOS_ACTIVITY=0``) has decided already; that
24
35
  is respected and nothing is touched.
@@ -35,7 +46,9 @@ import logging
35
46
  import os
36
47
  import platform
37
48
  import shutil
49
+ import re
38
50
  import subprocess
51
+ import sys
39
52
  from pathlib import Path
40
53
 
41
54
  from sdk.activity import settings
@@ -52,6 +65,8 @@ RULES_TEXT = (
52
65
  f"-a always,exit -F arch=b64 -S execve -k {RULE_KEY}\n"
53
66
  f"-a always,exit -F arch=b32 -S execve -k {RULE_KEY}\n"
54
67
  )
68
+ AUDITD_CONF = Path("/etc/audit/auditd.conf")
69
+ AUDIT_LOG = Path("/var/log/audit/audit.log")
55
70
  _LINUX_MANUAL = (
56
71
  "sudo apt install auditd # or: dnf install audit\n"
57
72
  f"sudo tee {RULES_PATH} <<'EOF'\n{RULES_TEXT}EOF\n"
@@ -112,6 +127,34 @@ def _is_privileged() -> bool:
112
127
  return hasattr(os, "geteuid") and os.geteuid() == 0
113
128
 
114
129
 
130
+ def _panel_user() -> tuple[str, Path, int, int] | None:
131
+ """``(name, home, uid, gid)`` of the account the panel runs as, if it is NOT root.
132
+
133
+ Under ``sudo`` that is ``SUDO_USER`` — the person who typed the command, whose
134
+ ``~/.agentos`` the panel reads. Otherwise it is the current user. ``None`` when the
135
+ panel itself is root, or on a platform without ``pwd``: root needs no grant.
136
+ """
137
+ try:
138
+ import pwd
139
+ except ImportError: # Windows
140
+ return None
141
+ sudo_user = os.environ.get("SUDO_USER", "").strip()
142
+ try:
143
+ if _is_privileged():
144
+ # Privileged WITHOUT sudo is a root panel (a system unit): nobody to grant,
145
+ # and no other home to write into. Never guess one from the real uid.
146
+ if not sudo_user or sudo_user == "root":
147
+ return None
148
+ pw = pwd.getpwnam(sudo_user)
149
+ else:
150
+ pw = pwd.getpwuid(os.getuid())
151
+ except (KeyError, OSError):
152
+ return None
153
+ if pw.pw_uid == 0:
154
+ return None
155
+ return pw.pw_name, Path(pw.pw_dir), pw.pw_uid, pw.pw_gid
156
+
157
+
115
158
  def _run(cmd: list[str], timeout: float = 60.0, env: dict | None = None) -> tuple[int, str]:
116
159
  """``(returncode, output)``; ``(-1, reason)`` when the command could not run."""
117
160
  try:
@@ -123,7 +166,29 @@ def _run(cmd: list[str], timeout: float = 60.0, env: dict | None = None) -> tupl
123
166
 
124
167
 
125
168
  def _env_file() -> Path:
126
- return Path.home() / ".agentos" / ".env"
169
+ """The ``.env`` the PANEL reads: the sudo-invoking user's, never root's by accident."""
170
+ user = _panel_user() if _is_privileged() else None
171
+ home = user[1] if user else Path.home()
172
+ return home / ".agentos" / ".env"
173
+
174
+
175
+ def _flag_value() -> str:
176
+ """``AGENTOS_ACTIVITY_AUDIT`` as the panel will see it: process env, then its ``.env``.
177
+
178
+ ``settings.setting`` reads ``Path.home()``'s ``.env``, which under ``sudo`` is root's
179
+ — not the file the panel reads — so the panel's file is read here directly.
180
+ """
181
+ val = os.environ.get(AUDIT_FLAG, "").strip()
182
+ if val:
183
+ return val
184
+ try:
185
+ for line in _env_file().read_text(encoding="utf-8").splitlines():
186
+ key, sep, value = line.strip().partition("=")
187
+ if sep and key.strip() == AUDIT_FLAG:
188
+ return value.strip().strip('"').strip("'")
189
+ except OSError:
190
+ pass
191
+ return settings.setting(AUDIT_FLAG)
127
192
 
128
193
 
129
194
  def _opted_out() -> bool:
@@ -131,7 +196,7 @@ def _opted_out() -> bool:
131
196
  return True
132
197
  falsey = {"0", "false", "no", "off"}
133
198
  return (settings.setting("AGENTOS_ACTIVITY").lower() in falsey
134
- or settings.setting(AUDIT_FLAG).lower() in falsey)
199
+ or _flag_value().lower() in falsey)
135
200
 
136
201
 
137
202
  def _enable_flag() -> bool:
@@ -140,14 +205,23 @@ def _enable_flag() -> bool:
140
205
  Left alone when the flag is set ANYWHERE (process env or .env) — an explicit
141
206
  value is the operator's, including an explicit "on".
142
207
  """
143
- if settings.setting(AUDIT_FLAG):
208
+ if _flag_value():
144
209
  return False
145
210
  path = _env_file()
146
211
  try:
212
+ created_dir = not path.parent.exists()
147
213
  path.parent.mkdir(parents=True, exist_ok=True)
148
214
  existing = path.read_text(encoding="utf-8") if path.exists() else ""
149
215
  sep = "" if (not existing or existing.endswith("\n")) else "\n"
150
216
  path.write_text(f"{existing}{sep}{AUDIT_FLAG}=1\n", encoding="utf-8")
217
+ # Written as root into a user's home: hand it back, or the panel (that user)
218
+ # could no longer edit its own .env — and a new ~/.agentos would be root's.
219
+ user = _panel_user() if _is_privileged() else None
220
+ if user:
221
+ _, _, uid, gid = user
222
+ if created_dir:
223
+ os.chown(path.parent, uid, gid)
224
+ os.chown(path, uid, gid)
151
225
  return True
152
226
  except OSError as exc:
153
227
  logger.warning("activity.os_setup.env_write_failed path=%s err=%s", path, exc)
@@ -215,9 +289,76 @@ def _ensure_linux() -> tuple[bool, list[str], str]:
215
289
  # Typically a container without audit support in its kernel namespace.
216
290
  return bool(done), done, f"audit rules did not load: {out.strip()[-300:]}"
217
291
  done.append("loaded audit rules")
292
+
293
+ user = _panel_user()
294
+ if user:
295
+ steps, err = _grant_log_read(user)
296
+ done.extend(steps)
297
+ if err:
298
+ return bool(done), done, err
218
299
  return bool(done), done, ""
219
300
 
220
301
 
302
+ def _grant_log_read(user: tuple[str, Path, int, int]) -> tuple[list[str], str]:
303
+ """Let the panel's (non-root) user read ``audit.log``. ``(steps_done, error)``.
304
+
305
+ ``log_group`` in ``auditd.conf`` is what survives rotation (auditd applies it to
306
+ every new log file and the directory); the chgrp/chmod makes the CURRENT file
307
+ readable now, without waiting for a rotation. Only a USER-PRIVATE group (named
308
+ like the user) is used: granting a shared group such as ``users`` would hand the
309
+ log of every command on the box to every member of it.
310
+ """
311
+ import grp
312
+
313
+ name, _, _, gid = user
314
+ try:
315
+ group = grp.getgrgid(gid).gr_name
316
+ except KeyError:
317
+ group = ""
318
+ if group != name:
319
+ return [], (f"{name}'s primary group is {group or gid}, not a private group — "
320
+ f"set log_group in {AUDITD_CONF} to a group only {name} is in")
321
+ done: list[str] = []
322
+ try:
323
+ text = AUDITD_CONF.read_text(encoding="utf-8") if AUDITD_CONF.exists() else ""
324
+ lines = text.splitlines(keepends=True)
325
+ found = changed = False
326
+ for i, line in enumerate(lines):
327
+ if re.match(r"\s*log_group\s*=", line):
328
+ found = True
329
+ if line.split("=", 1)[1].strip() != group:
330
+ lines[i] = f"log_group = {group}\n"
331
+ changed = True
332
+ if not found:
333
+ sep = "" if (not lines or lines[-1].endswith("\n")) else "\n"
334
+ lines.append(f"{sep}log_group = {group}\n")
335
+ changed = True
336
+ if changed:
337
+ AUDITD_CONF.write_text("".join(lines), encoding="utf-8")
338
+ # auditd re-reads its config on SIGHUP; `auditctl --signal` is audit ≥ 3.
339
+ if _run(["auditctl", "--signal", "reload"])[0] != 0:
340
+ _run(["pkill", "-HUP", "-x", "auditd"])
341
+ done.append(f"log_group = {group}")
342
+ for path, bits in ((AUDIT_LOG.parent, 0o050), (AUDIT_LOG, 0o040)):
343
+ if not path.exists():
344
+ continue
345
+ st = path.stat()
346
+ if st.st_gid != gid:
347
+ os.chown(path, -1, gid)
348
+ if st.st_mode & bits != bits:
349
+ os.chmod(path, (st.st_mode & 0o7777) | bits)
350
+ if st.st_gid != gid or st.st_mode & bits != bits:
351
+ if f"{AUDIT_LOG} readable by {name}" not in done:
352
+ done.append(f"{AUDIT_LOG} readable by {name}")
353
+ except OSError as exc:
354
+ return done, f"could not make {AUDIT_LOG} readable by {name}: {exc}"
355
+ return done, ""
356
+
357
+
358
+ def _log_readable() -> bool:
359
+ return os.access(AUDIT_LOG, os.R_OK)
360
+
361
+
221
362
  # ── Windows steps ───────────────────────────────────────────────────────────
222
363
 
223
364
  def _ensure_windows() -> tuple[bool, list[str], str]:
@@ -389,10 +530,14 @@ def ensure(apply: bool = True) -> tuple[bool, str]:
389
530
  manual = {"linux": _LINUX_MANUAL, "windows": _WIN_MANUAL, "darwin": _MAC_MANUAL}[sysname]
390
531
 
391
532
  if not apply or not _is_privileged():
392
- if settings.audit_enabled():
393
- return False, "" # someone already set it up by hand
533
+ ready = settings.audit_enabled() and (sysname != "linux" or _log_readable())
534
+ if ready:
535
+ return False, "" # set up already (by hand, or by an earlier run)
536
+ if apply and sysname == "linux" and _sudo_without_password():
537
+ return _ensure_via_sudo()
394
538
  return False, ("activity audit is not set up — human activity on this VPS is "
395
- "not being recorded. Run as root/Administrator:\n" + manual)
539
+ f"not being recorded. Run once:\n sudo {_agentos_exe()} "
540
+ "activity-setup\nor by hand:\n" + manual)
396
541
 
397
542
  step = {"linux": _ensure_linux, "windows": _ensure_windows, "darwin": _ensure_macos}
398
543
  _, done, err = step[sysname]()
@@ -415,6 +560,41 @@ def ensure(apply: bool = True) -> tuple[bool, str]:
415
560
  return False, f"activity audit setup failed: {exc}"
416
561
 
417
562
 
563
+ def _sudo_without_password() -> bool:
564
+ """Can this (unprivileged) process become root with no prompt? Never prompts."""
565
+ if not shutil.which("sudo"):
566
+ return False
567
+ return _run(["sudo", "-n", "true"], timeout=15)[0] == 0
568
+
569
+
570
+ def _ensure_via_sudo() -> tuple[bool, str]:
571
+ """Run the privileged half as root through ``sudo -n``, as THIS user's ``SUDO_USER``.
572
+
573
+ The root run writes the flag into this user's ``.env`` and grants this user the
574
+ audit log, so the panel (this user) picks both up. Same interpreter, so the same
575
+ installation is what runs — never whatever ``agentos`` is first on root's PATH.
576
+ """
577
+ before = settings.audit_enabled()
578
+ rc, out = _run(["sudo", "-n", sys.executable, "-m", "sdk.activity.os_setup"], timeout=1800)
579
+ lines = [ln.strip() for ln in out.splitlines() if ln.strip()]
580
+ # cli() prints the outcome first ("activity audit …") and a restart hint after it.
581
+ last = next((ln for ln in lines if ln.startswith("activity audit")), lines[-1] if lines else "")
582
+ if rc != 0:
583
+ logger.warning("activity.os_setup.sudo_failed rc=%s out=%s", rc, out[-300:])
584
+ return False, f"activity audit setup incomplete (via sudo): {last or f'exit {rc}'}"
585
+ changed = settings.audit_enabled() and not before
586
+ return changed, (last if last and "already set up" not in last else
587
+ ("activity audit enabled (via sudo)" if changed else ""))
588
+
589
+
590
+ def _agentos_exe() -> str:
591
+ """Absolute ``agentos`` path, so ``sudo`` finds the venv / ~/.local/bin install."""
592
+ exe = Path(sys.executable).parent / ("agentos.exe" if _system() == "windows" else "agentos")
593
+ if exe.exists():
594
+ return str(exe)
595
+ return shutil.which("agentos") or "agentos"
596
+
597
+
418
598
  def cli(argv: list[str]) -> int:
419
599
  """``agentos activity-setup [--check]`` — run (or just report) the OS audit setup."""
420
600
  changed, msg = ensure(apply="--check" not in argv)
@@ -424,5 +604,11 @@ def cli(argv: list[str]) -> int:
424
604
  print("activity audit: already set up" if settings.audit_enabled()
425
605
  else "activity audit: nothing to do on this platform")
426
606
  if changed:
427
- print("restart the panel for it to take effect: sudo systemctl restart agentos-ui")
607
+ print("restart the panel for it to take effect: systemctl --user restart agentos-ui "
608
+ "(or sudo systemctl restart agentos-ui for a system service). A running panel "
609
+ "also turns the collector on by itself at its next start.")
428
610
  return 1 if "incomplete" in msg or "failed" in msg else 0
611
+
612
+
613
+ if __name__ == "__main__": # `sudo -n python -m sdk.activity.os_setup` (see _ensure_via_sudo)
614
+ sys.exit(cli(sys.argv[1:]))
@@ -9,7 +9,9 @@ Per platform:
9
9
  ``loginctl enable-linger`` so it runs without an active login.
10
10
  ``Restart=always`` gives crash-recovery; ``enable`` gives boot-start.
11
11
  * macOS — a launchd LaunchAgent with ``RunAtLoad`` + ``KeepAlive``.
12
- * Windows — a Scheduled Task (``schtasks``) that runs the panel at logon.
12
+ * Windows — a Scheduled Task (``schtasks``) that runs the panel at logon, plus a
13
+ second, daily task that runs ``agentos-auto update`` (the systemd
14
+ timer's counterpart — without it a Windows box never updated itself).
13
15
 
14
16
  Everything is best-effort and returns ``(ok, message)`` so the caller (the
15
17
  ``agentos-ui install`` command and the bootstrap scripts) can report cleanly.
@@ -28,6 +30,7 @@ IS_MAC = sys.platform == "darwin"
28
30
 
29
31
  SERVICE_NAME = "agentos-ui" # systemd unit / launchd label base
30
32
  WIN_TASK = "AgentOS Control Panel" # Windows scheduled-task name
33
+ WIN_UPDATE_TASK = "AgentOS Self-Update" # Windows counterpart of agentos-update.timer
31
34
 
32
35
 
33
36
  def _exe() -> str:
@@ -166,7 +169,7 @@ def _auto_exe() -> str:
166
169
  minimal environment, and picking up a different venv's console script is how
167
170
  an updater ends up upgrading the wrong installation.
168
171
  """
169
- candidate = Path(sys.executable).parent / "agentos-auto"
172
+ candidate = Path(sys.executable).parent / ("agentos-auto.exe" if IS_WINDOWS else "agentos-auto")
170
173
  if candidate.exists():
171
174
  return str(candidate)
172
175
  return shutil.which("agentos-auto") or "agentos-auto"
@@ -174,6 +177,8 @@ def _auto_exe() -> str:
174
177
 
175
178
  def update_timer_installed() -> bool:
176
179
  """Is the daily self-update timer present on this box?"""
180
+ if IS_WINDOWS:
181
+ return _run(["schtasks", "/Query", "/TN", WIN_UPDATE_TASK])[0] == 0
177
182
  if not _systemd_available():
178
183
  return False
179
184
  is_root = hasattr(os, "geteuid") and os.geteuid() == 0
@@ -185,6 +190,8 @@ def update_timer_installed() -> bool:
185
190
  def _install_update_timer() -> tuple[bool, str]:
186
191
  """Install (or refresh) the daily self-update timer. Best-effort by design —
187
192
  the panel must install even on a box where the timer cannot."""
193
+ if IS_WINDOWS:
194
+ return _install_update_schtask()
188
195
  if not _systemd_available():
189
196
  return False, "no systemd — skipping the self-update timer"
190
197
  home = str(Path.home())
@@ -378,7 +385,82 @@ def _install_schtask(host: str, port: int) -> tuple[bool, str]:
378
385
  f"'Run whether user is logged on' + 'Restart on failure' in Task Scheduler.")
379
386
 
380
387
 
388
+ def _update_task_xml(exe: str, home: str) -> str:
389
+ """Task Scheduler XML for the daily self-update — the systemd timer, translated.
390
+
391
+ XML rather than ``schtasks`` flags because the flags cannot express the two
392
+ settings that matter most: ``RandomDelay`` (the fleet must not clone from GitHub
393
+ in the same second) and ``StartWhenAvailable`` (a box that was off at the
394
+ scheduled time updates when it is next up — systemd's ``Persistent=true``).
395
+
396
+ It runs as the installing user with the highest available privileges, same as
397
+ the panel task: ``~/.agentos`` (token, ``.env``) lives in that user's profile, and
398
+ the updater's activity-audit step needs an elevated token to configure auditpol.
399
+ """
400
+ from xml.sax.saxutils import escape
401
+
402
+ delay_min = max(1, UPDATE_RANDOM_DELAY_S // 60)
403
+ return (
404
+ '<?xml version="1.0" encoding="UTF-16"?>\n'
405
+ '<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">\n'
406
+ " <RegistrationInfo><Description>AgentOS SDK self-update (daily)</Description>"
407
+ "</RegistrationInfo>\n"
408
+ " <Triggers>\n"
409
+ " <CalendarTrigger>\n"
410
+ " <StartBoundary>2026-01-01T03:00:00</StartBoundary>\n"
411
+ f" <RandomDelay>PT{delay_min}M</RandomDelay>\n"
412
+ " <ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay>\n"
413
+ " </CalendarTrigger>\n"
414
+ " </Triggers>\n"
415
+ " <Principals><Principal id=\"Author\">"
416
+ "<LogonType>InteractiveToken</LogonType><RunLevel>HighestAvailable</RunLevel>"
417
+ "</Principal></Principals>\n"
418
+ " <Settings>\n"
419
+ " <StartWhenAvailable>true</StartWhenAvailable>\n"
420
+ " <RunOnlyIfNetworkAvailable>true</RunOnlyIfNetworkAvailable>\n"
421
+ " <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>\n"
422
+ " <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>\n"
423
+ " <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>\n"
424
+ " <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>\n"
425
+ " </Settings>\n"
426
+ ' <Actions Context="Author"><Exec>'
427
+ f"<Command>{escape(exe)}</Command><Arguments>update</Arguments>"
428
+ f"<WorkingDirectory>{escape(home)}</WorkingDirectory>"
429
+ "</Exec></Actions>\n"
430
+ "</Task>\n"
431
+ )
432
+
433
+
434
+ def _install_update_schtask() -> tuple[bool, str]:
435
+ """Create (or replace) the daily self-update task. Best-effort, like the timer.
436
+
437
+ A SEPARATE task from the panel's for the same reason the systemd updater is a
438
+ separate unit: the update restarts the panel task, and an updater running inside
439
+ it would be ended part way through its own ``pip install``.
440
+ """
441
+ import tempfile
442
+
443
+ xml = _update_task_xml(_auto_exe(), str(Path.home()))
444
+ fd, path = tempfile.mkstemp(suffix=".xml")
445
+ try:
446
+ with os.fdopen(fd, "w", encoding="utf-16") as fh: # schtasks /XML wants UTF-16
447
+ fh.write(xml)
448
+ rc, out = _run(["schtasks", "/Create", "/TN", WIN_UPDATE_TASK, "/XML", path, "/F"])
449
+ except OSError as exc:
450
+ return False, f"could not write the self-update task: {exc}"
451
+ finally:
452
+ try:
453
+ os.unlink(path)
454
+ except OSError:
455
+ pass
456
+ if rc != 0:
457
+ return False, f"could not create the self-update task:\n{out}"
458
+ return True, (f"self-update task '{WIN_UPDATE_TASK}' created — "
459
+ f"daily, randomized, catches up after downtime")
460
+
461
+
381
462
  def _uninstall_schtask() -> tuple[bool, str]:
463
+ _run(["schtasks", "/Delete", "/TN", WIN_UPDATE_TASK, "/F"])
382
464
  _run(["schtasks", "/End", "/TN", WIN_TASK])
383
465
  rc, out = _run(["schtasks", "/Delete", "/TN", WIN_TASK, "/F"])
384
466
  return (rc == 0), (f"removed scheduled task '{WIN_TASK}'." if rc == 0 else out)
@@ -388,7 +470,9 @@ def _uninstall_schtask() -> tuple[bool, str]:
388
470
 
389
471
  def install(host: str = "0.0.0.0", port: int = 8765) -> tuple[bool, str]:
390
472
  if IS_WINDOWS:
391
- return _install_schtask(host, port)
473
+ ok, msg = _install_schtask(host, port)
474
+ timer_ok, timer_msg = _install_update_timer()
475
+ return ok, f"{msg}\n {'' if timer_ok else 'note: '}{timer_msg}"
392
476
  if IS_MAC:
393
477
  return _install_launchd(host, port)
394
478
  if _systemd_available():