devops-bot-sdk 1.6.52__tar.gz → 1.6.54__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/__init__.py +1 -1
  4. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/collectors/macos_eslogger.py +11 -0
  5. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agentd.py +48 -11
  6. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/platform_compat.py +12 -3
  7. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/run_auto.py +63 -17
  8. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_activity_macos.py +31 -0
  9. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_run_auto_agent_only.py +86 -0
  10. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/README.md +0 -0
  11. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  12. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  13. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  14. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/devops_bot_sdk.egg-info/requires.txt +0 -0
  15. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  16. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/pyproject.toml +0 -0
  17. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/__init__.py +0 -0
  18. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/classifier.py +0 -0
  19. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/collectors/__init__.py +0 -0
  20. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/collectors/common.py +0 -0
  21. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/collectors/linux_auditd.py +0 -0
  22. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/collectors/windows_eventlog.py +0 -0
  23. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/models.py +0 -0
  24. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/os_setup.py +0 -0
  25. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/recorder.py +0 -0
  26. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/redact.py +0 -0
  27. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/settings.py +0 -0
  28. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/shipper.py +0 -0
  29. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/activity/spool.py +0 -0
  30. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/__init__.py +0 -0
  31. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/activity.py +0 -0
  32. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/builder.py +0 -0
  33. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/chat.py +0 -0
  34. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/connectors.py +0 -0
  35. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/engines/__init__.py +0 -0
  36. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/engines/claude_code.py +0 -0
  37. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/engines/http.py +0 -0
  38. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/engines/mcp.py +0 -0
  39. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/engines/n8n.py +0 -0
  40. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/engines/shell.py +0 -0
  41. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/integrations.py +0 -0
  42. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/inventory.py +0 -0
  43. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/library/__init__.py +0 -0
  44. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/manifest.py +0 -0
  45. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/prompt_studio.py +0 -0
  46. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/prompt_template.py +0 -0
  47. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/prompt_vars.py +0 -0
  48. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/run_log.py +0 -0
  49. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/schedules.py +0 -0
  50. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/secrets.py +0 -0
  51. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/ticket_run.py +0 -0
  52. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/usage.py +0 -0
  53. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/agents/varstore.py +0 -0
  54. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/browser_profile.py +0 -0
  55. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/cli.py +0 -0
  56. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/client.py +0 -0
  57. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/collectors/__init__.py +0 -0
  58. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/collectors/files.py +0 -0
  59. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/collectors/process.py +0 -0
  60. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/collectors/screenshot.py +0 -0
  61. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/config.py +0 -0
  62. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/crucial.py +0 -0
  63. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/departments.py +0 -0
  64. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/design_verify/__init__.py +0 -0
  65. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/design_verify/assets.py +0 -0
  66. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/design_verify/bootstrap.py +0 -0
  67. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/design_verify/browser.py +0 -0
  68. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/design_verify/compare.py +0 -0
  69. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/design_verify/loop.py +0 -0
  70. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/estimator.py +0 -0
  71. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/exceptions.py +0 -0
  72. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/firewall.py +0 -0
  73. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/formats.py +0 -0
  74. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/git_ops.py +0 -0
  75. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/github_fetch.py +0 -0
  76. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/github_ingest.py +0 -0
  77. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/graph_recon.py +0 -0
  78. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/graphify.py +0 -0
  79. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/hooks/__init__.py +0 -0
  80. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/hooks/crucial_guard.py +0 -0
  81. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/intake.py +0 -0
  82. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ipc/__init__.py +0 -0
  83. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ipc/electron_bridge.py +0 -0
  84. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ipc/handlers.py +0 -0
  85. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/journal.py +0 -0
  86. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/local_exec.py +0 -0
  87. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/memory_guard.py +0 -0
  88. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/models/__init__.py +0 -0
  89. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/models/envelope.py +0 -0
  90. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/models/requests.py +0 -0
  91. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/models/responses.py +0 -0
  92. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/models/snapshots.py +0 -0
  93. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/phase_timer.py +0 -0
  94. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/plan_run.py +0 -0
  95. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/prd_progress.py +0 -0
  96. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/proof_readers.py +0 -0
  97. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/providers.py +0 -0
  98. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/py.typed +0 -0
  99. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/resume.py +0 -0
  100. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/router.py +0 -0
  101. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/scheduler.py +0 -0
  102. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/self_report.py +0 -0
  103. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/self_report_local.py +0 -0
  104. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/sse.py +0 -0
  105. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/swagger.py +0 -0
  106. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/task_file.py +0 -0
  107. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/task_types.py +0 -0
  108. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/test.py +0 -0
  109. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/test_pipeline.py +0 -0
  110. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ticket_fields.py +0 -0
  111. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/tiers/__init__.py +0 -0
  112. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/tiers/mini_agent.py +0 -0
  113. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/__init__.py +0 -0
  114. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/api.py +0 -0
  115. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/dashboard.py +0 -0
  116. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/health.py +0 -0
  117. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/ip_allowlist.py +0 -0
  118. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/jobs.py +0 -0
  119. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/models.py +0 -0
  120. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/runner.py +0 -0
  121. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/security.py +0 -0
  122. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/server.py +0 -0
  123. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/service.py +0 -0
  124. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/session.py +0 -0
  125. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/setup_manager.py +0 -0
  126. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/agents.html +0 -0
  127. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/app.js +0 -0
  128. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/chat.html +0 -0
  129. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/custom-agents.html +0 -0
  130. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/default-agents.html +0 -0
  131. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/index.html +0 -0
  132. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/report.html +0 -0
  133. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/runs.html +0 -0
  134. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/schedules.html +0 -0
  135. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/setup.html +0 -0
  136. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/styles.css +0 -0
  137. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/static/tickets.html +0 -0
  138. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/ui/swagger.py +0 -0
  139. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/updater.py +0 -0
  140. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/url_access.py +0 -0
  141. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/vps_health.py +0 -0
  142. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/sdk/write_hook.py +0 -0
  143. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/setup.cfg +0 -0
  144. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_activity_collectors.py +0 -0
  145. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_activity_core.py +0 -0
  146. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_activity_os_setup.py +0 -0
  147. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_activity_pipeline.py +0 -0
  148. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_agent_activity.py +0 -0
  149. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_agent_activity_tracking.py +0 -0
  150. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_agent_chat_features.py +0 -0
  151. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_agent_listing.py +0 -0
  152. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_agent_prompt_async.py +0 -0
  153. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_agent_prompt_models.py +0 -0
  154. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_agent_run_ticket.py +0 -0
  155. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_agent_schedules.py +0 -0
  156. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_agent_workspace_settings.py +0 -0
  157. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_agents_connectors_builder.py +0 -0
  158. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_allowed_tools_config.py +0 -0
  159. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_auto_agent_dispatch.py +0 -0
  160. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_browser_profile.py +0 -0
  161. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_cheap_tier.py +0 -0
  162. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_client_jira_projects.py +0 -0
  163. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_config_token_diagnostics.py +0 -0
  164. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_configure_token_owner.py +0 -0
  165. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_console_encoding.py +0 -0
  166. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_crash_recovery.py +0 -0
  167. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_departments.py +0 -0
  168. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_done_transition.py +0 -0
  169. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_epic_decompose_run.py +0 -0
  170. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_epic_inherited_agent_id.py +0 -0
  171. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_epic_session_isolation.py +0 -0
  172. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_estimator.py +0 -0
  173. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_firewall_open_on_update.py +0 -0
  174. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_formats.py +0 -0
  175. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_git_action.py +0 -0
  176. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_github_fetch.py +0 -0
  177. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_github_ingest.py +0 -0
  178. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_graph_recon.py +0 -0
  179. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_graphify_build.py +0 -0
  180. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_graphify_hook_install.py +0 -0
  181. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_health.py +0 -0
  182. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_intake_job_outcome.py +0 -0
  183. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_intake_plan_review.py +0 -0
  184. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_journal.py +0 -0
  185. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_local_exec_provider.py +0 -0
  186. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_mcp_route_contract.py +0 -0
  187. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_memory_guard.py +0 -0
  188. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_phase_timer.py +0 -0
  189. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_plan_new_command.py +0 -0
  190. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_plan_run_activity.py +0 -0
  191. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_pr_identity.py +0 -0
  192. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_prd_artifact.py +0 -0
  193. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_project_path_resolution.py +0 -0
  194. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_proof_readers.py +0 -0
  195. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_providers.py +0 -0
  196. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_recon_session_isolation.py +0 -0
  197. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_resume.py +0 -0
  198. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_router.py +0 -0
  199. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_run_outputs.py +0 -0
  200. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_run_verdict_is_honest.py +0 -0
  201. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_schedule_api.py +0 -0
  202. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_schedule_outputs_and_plans.py +0 -0
  203. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_scheduler.py +0 -0
  204. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_self_report.py +0 -0
  205. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_service_token_warning.py +0 -0
  206. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_stale_panel_restart.py +0 -0
  207. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_swagger_command.py +0 -0
  208. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_task_file_and_plan_run.py +0 -0
  209. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_task_file_format_doc.py +0 -0
  210. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_ticket_fields.py +0 -0
  211. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_ticket_run_resolve_by_id.py +0 -0
  212. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_ticket_speed_fixes.py +0 -0
  213. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_tier_flag_parity.py +0 -0
  214. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_ui_gate.py +0 -0
  215. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_ui_lifespan.py +0 -0
  216. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_update_timer.py +0 -0
  217. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_updater_extras.py +0 -0
  218. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_updater_pep668.py +0 -0
  219. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_updater_reports_the_truth.py +0 -0
  220. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_url_access.py +0 -0
  221. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_vps_health.py +0 -0
  222. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/tests/test_windows_loop_identity.py +0 -0
  223. {devops_bot_sdk-1.6.52 → devops_bot_sdk-1.6.54}/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.52
3
+ Version: 1.6.54
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.52
3
+ Version: 1.6.54
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
@@ -34,7 +34,7 @@ Rules:
34
34
  - All data egress through submit_webhook only
35
35
  """
36
36
 
37
- __version__ = "1.6.52" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.54" # 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
@@ -167,6 +167,17 @@ class EsloggerParser:
167
167
  def flush(self) -> None:
168
168
  """Nothing is buffered — every eslogger line is a whole event."""
169
169
 
170
+ def flush_older_than(self, cutoff: float) -> None:
171
+ """No-op, for the same reason as :meth:`flush`: nothing is held.
172
+
173
+ :class:`EsloggerCollector` reuses :class:`AuditdCollector`'s tail loop, which
174
+ calls this on the parser every poll to close auditd events whose trailing
175
+ records never arrived. An eslogger line is already a whole event, so there is
176
+ nothing to time-flush — but the method MUST exist, or the shared loop raises
177
+ ``AttributeError`` on its first idle poll and the whole collector thread dies
178
+ before a single event is recorded (the tail starts at EOF, so the very first
179
+ poll reads nothing and goes straight here)."""
180
+
170
181
  # ── sessions ─────────────────────────────────────────────────────────────
171
182
 
172
183
  def _asid_session(self, asid: int, ts: float) -> str:
@@ -199,6 +199,11 @@ def _daemon_running() -> int | None:
199
199
  return None
200
200
 
201
201
 
202
+ #: Set in the environment right before the macOS re-exec below, read back by the
203
+ #: fresh interpreter so it knows it IS the detached daemon and must not fork again.
204
+ _DAEMONIZED_ENV = "AGENTOS_AGENTD_DAEMONIZED"
205
+
206
+
202
207
  def _daemonize() -> bool:
203
208
  """Detach the current process from the terminal (POSIX double-fork).
204
209
 
@@ -207,10 +212,23 @@ def _daemonize() -> bool:
207
212
  _LOG_FILE and a PID file is written so `status` can find the daemon.
208
213
  Returns False (stays in the foreground) on platforms without os.fork
209
214
  (e.g. Windows).
215
+
216
+ On macOS the detached child re-execs a fresh interpreter instead of running
217
+ the daemon in-process (see below); ``main()`` re-enters here with the
218
+ ``_DAEMONIZED_ENV`` marker set, where setsid, the redirected fds 0/1/2 and the
219
+ PID file already survive the exec, so we only re-point the stream wrappers.
210
220
  """
211
221
  if not hasattr(os, "fork"):
212
222
  return False # caller falls back to foreground
213
223
 
224
+ if os.environ.get(_DAEMONIZED_ENV) == "1":
225
+ # Don't leak the one-shot marker to subprocesses the daemon later spawns
226
+ # (claude, git, …) — they must never think THEY are the detached daemon.
227
+ os.environ.pop(_DAEMONIZED_ENV, None)
228
+ sys.stdout = open(_LOG_FILE, "a", buffering=1, encoding="utf-8")
229
+ sys.stderr = sys.stdout
230
+ return True
231
+
214
232
  _AGENTOS_DIR.mkdir(parents=True, exist_ok=True)
215
233
 
216
234
  # First fork: let the shell reap the parent and return the prompt.
@@ -232,13 +250,27 @@ def _daemonize() -> bool:
232
250
  os.close(devnull)
233
251
  os.close(logfd)
234
252
 
253
+ _PID_FILE.write_text(str(os.getpid()))
254
+
255
+ # macOS: re-exec a FRESH interpreter so the daemon's networking runs in a clean
256
+ # process image. A bare double-fork() with NO exec() leaves Apple's higher-level
257
+ # frameworks (Network.framework DNS/NAT64, the libsystem_trace os_log cache)
258
+ # half-initialised in the child, and the first getaddrinfo on the asyncio resolver
259
+ # thread then SIGSEGVs ("crashed on child side of fork pre-exec"). OBJC_DISABLE_
260
+ # INITIALIZE_FORK_SAFETY silences only the objc guard, not these — so it is not
261
+ # enough on macOS 26. The exec'd process keeps this PID, setsid session and fds.
262
+ if sys.platform == "darwin":
263
+ os.environ[_DAEMONIZED_ENV] = "1"
264
+ try:
265
+ os.execv(sys.executable, [sys.executable, sys.argv[0], *sys.argv[1:]])
266
+ except Exception: # noqa: BLE001 — exec failed; fall back to in-process daemon
267
+ os.environ.pop(_DAEMONIZED_ENV, None)
268
+
235
269
  # Line-buffer stdout/stderr so progress reaches the log live rather than
236
270
  # sitting in Python's block-buffer for a non-tty stream (same fix
237
271
  # sdk/run_auto.py applies for the same reason).
238
272
  sys.stdout = open(_LOG_FILE, "a", buffering=1, encoding="utf-8")
239
273
  sys.stderr = sys.stdout
240
-
241
- _PID_FILE.write_text(str(os.getpid()))
242
274
  return True
243
275
 
244
276
 
@@ -736,17 +768,22 @@ class AgentDaemon:
736
768
 
737
769
  def start(foreground: bool = False) -> None:
738
770
  """Start agentd — in the background by default, attached with --foreground."""
771
+ # macOS re-exec of the already-detached daemon (see _daemonize): this process
772
+ # IS the daemon — the duplicate guard, banner and stop-flag clear all ran in
773
+ # the original process before the fork, so skip straight to re-detaching.
774
+ resuming = os.environ.get(_DAEMONIZED_ENV) == "1"
739
775
  if not foreground:
740
- pid = _daemon_running()
741
- if pid:
742
- print(f"agentd is already running in the background (pid {pid}).")
743
- print(f" logs: {_LOG_FILE}")
744
- return
776
+ if not resuming:
777
+ pid = _daemon_running()
778
+ if pid:
779
+ print(f"agentd is already running in the background (pid {pid}).")
780
+ print(f" logs: {_LOG_FILE}")
781
+ return
745
782
 
746
- print("agentd starting in the background — it keeps running after you "
747
- "close this terminal.")
748
- print(f" logs: {_LOG_FILE}")
749
- _clear_stop() # a fresh start clears any stale stop flag
783
+ print("agentd starting in the background — it keeps running after you "
784
+ "close this terminal.")
785
+ print(f" logs: {_LOG_FILE}")
786
+ _clear_stop() # a fresh start clears any stale stop flag
750
787
  if not _daemonize():
751
788
  print("note: background mode unavailable on this platform — running "
752
789
  "in the foreground.")
@@ -80,12 +80,21 @@ def ensure_utf8_stdio() -> None:
80
80
  def ensure_macos_fork_safety() -> None:
81
81
  """Stop macOS from killing our background daemons with a native abort.
82
82
 
83
- ``agentos-auto`` / ``agentos-agentd`` double-fork (fork + setsid, no exec)
84
- into a background daemon that then does networking (httpx → CFNetwork /
83
+ ``agentos-auto`` / ``agentos-agentd`` double-fork (fork + setsid) into a
84
+ background daemon that then does networking (httpx → CFNetwork /
85
85
  SystemConfiguration) and TLS. When the Objective-C runtime detects objc use
86
86
  after a bare ``fork()`` with no ``exec()`` it calls ``abort()`` — which macOS
87
87
  surfaces as the "Python quit unexpectedly" crash dialog. Exporting
88
- ``OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES`` disables that guard.
88
+ ``OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES`` disables that *objc* guard.
89
+
90
+ It is NOT sufficient on its own: Apple's higher-level frameworks (Network.
91
+ framework's DNS/NAT64 path, the libsystem_trace ``os_log`` cache) are also
92
+ fork-unsafe and will SIGSEGV in the forked child on the first ``getaddrinfo``
93
+ — the env var does not cover them. The actual fix for that is in each daemon's
94
+ ``_daemonize()``, which re-execs a fresh interpreter after the double-fork so
95
+ the daemon runs in a clean process image. This shim remains the belt to that
96
+ braces: it quiets the objc guard and short-circuits the ``_scproxy`` lookup for
97
+ any pre-fork or non-daemon objc use, and seeds the var for the exec'd child.
89
98
 
90
99
  The objc runtime reads that variable when its dylib is first mapped (that is
91
100
  interpreter startup on framework Python builds), so setting it via
@@ -330,6 +330,23 @@ def _daemon_running() -> int | None:
330
330
  return None
331
331
 
332
332
 
333
+ #: Set in the environment right before the macOS re-exec below, read back by the
334
+ #: fresh interpreter so it knows it IS the detached daemon and must not fork again.
335
+ _DAEMONIZED_ENV = "AGENTOS_AUTO_DAEMONIZED"
336
+
337
+
338
+ def _relog_line_buffered() -> None:
339
+ """Point sys.stdout/stderr at _LOG_FILE line-buffered.
340
+
341
+ When stdout is a file (not a tty) Python block-buffers it, so progress printed
342
+ via _send() would sit in the buffer and never reach the log until the process
343
+ exits — making the daemon look idle. A line-buffered writer flushes every
344
+ progress line live, exactly like the foreground run.
345
+ """
346
+ sys.stdout = open(_LOG_FILE, "a", buffering=1, encoding="utf-8")
347
+ sys.stderr = sys.stdout
348
+
349
+
333
350
  def _daemonize() -> bool:
334
351
  """Detach the current process from the terminal (POSIX double-fork).
335
352
 
@@ -337,10 +354,23 @@ def _daemonize() -> bool:
337
354
  os._exit() and never return. stdout/stderr are redirected to _LOG_FILE and a
338
355
  PID file is written so `stop`/`status` can find the loop. Returns False (stays
339
356
  in the foreground) on platforms without os.fork (e.g. Windows).
357
+
358
+ On macOS the detached child does NOT run the loop in-process: it re-execs a
359
+ fresh interpreter (see below) and ``main()`` re-enters here with the
360
+ ``_DAEMONIZED_ENV`` marker set — at which point setsid, the redirected fds
361
+ 0/1/2 and the PID file already survive from before the exec, so we only
362
+ re-point the Python stream wrappers and report "I am the daemon".
340
363
  """
341
364
  if not hasattr(os, "fork"):
342
365
  return False # caller falls back to foreground
343
366
 
367
+ if os.environ.get(_DAEMONIZED_ENV) == "1":
368
+ # Don't leak the one-shot marker to subprocesses the loop later spawns
369
+ # (claude, git, …) — they must never think THEY are the detached daemon.
370
+ os.environ.pop(_DAEMONIZED_ENV, None)
371
+ _relog_line_buffered()
372
+ return True
373
+
344
374
  _AGENTOS_DIR.mkdir(parents=True, exist_ok=True)
345
375
 
346
376
  # First fork: let the shell reap the parent and return the prompt.
@@ -362,15 +392,24 @@ def _daemonize() -> bool:
362
392
  os.close(devnull)
363
393
  os.close(logfd)
364
394
 
365
- # CRITICAL: when stdout is a file (not a tty) Python block-buffers it, so
366
- # progress printed via _send() would sit in the buffer and never reach the
367
- # log until the process exits — making the daemon look idle. Replace the
368
- # streams with line-buffered writers so every progress line is flushed live,
369
- # exactly like the foreground run.
370
- sys.stdout = open(_LOG_FILE, "a", buffering=1, encoding="utf-8")
371
- sys.stderr = sys.stdout
372
-
373
395
  _write_pid()
396
+
397
+ # macOS: re-exec a FRESH interpreter so the daemon that does networking runs in
398
+ # a clean process image. A bare double-fork() with NO exec() leaves Apple's
399
+ # higher-level frameworks (Network.framework's DNS/NAT64 path, the libsystem_trace
400
+ # os_log cache) half-initialised in the child, and the FIRST getaddrinfo on the
401
+ # asyncio resolver thread then SIGSEGVs ("crashed on child side of fork pre-exec").
402
+ # OBJC_DISABLE_INITIALIZE_FORK_SAFETY only silences the objc +initialize guard,
403
+ # NOT these frameworks — so the env var alone is not enough on macOS 26. The exec'd
404
+ # process keeps this PID, this setsid session and the fds 0/1/2 redirected above.
405
+ if sys.platform == "darwin":
406
+ os.environ[_DAEMONIZED_ENV] = "1"
407
+ try:
408
+ os.execv(sys.executable, [sys.executable, sys.argv[0], *sys.argv[1:]])
409
+ except Exception: # noqa: BLE001 — exec failed; fall back to in-process daemon
410
+ os.environ.pop(_DAEMONIZED_ENV, None)
411
+
412
+ _relog_line_buffered()
374
413
  return True
375
414
 
376
415
 
@@ -1460,6 +1499,11 @@ def main() -> None:
1460
1499
  # ticket K... → ONLY the named Jira keys (any status, in order)
1461
1500
  # story [K...] → convert Stories → Tasks only (with PRD); NO coding.
1462
1501
  # With keys: those Stories; without: every To Do Story.
1502
+ # macOS re-exec of the already-detached daemon (see _daemonize): this
1503
+ # process IS the loop owner — the update, the duplicate-loop guard and the
1504
+ # banner all ran in the original process before the fork, so skip them and
1505
+ # go straight to detaching (a no-op fork here) and running the loop.
1506
+ resuming = os.environ.get(_DAEMONIZED_ENV) == "1"
1463
1507
  only_keys: list[str] | None = None
1464
1508
  stories_only = cmd in ("story", "stories")
1465
1509
  # `--agent-only` narrows whichever mode was chosen to the agent-tagged
@@ -1480,17 +1524,19 @@ def main() -> None:
1480
1524
  # long-lived old daemon would pin `stop`/`status` to a stale version. Runs
1481
1525
  # in the FOREGROUND so the user sees update output; re-execs on the new
1482
1526
  # code. Skipped with --no-update or AGENTOS_NO_UPDATE=1.
1483
- if "--no-update" not in argv:
1527
+ if not resuming and "--no-update" not in argv:
1484
1528
  from sdk.updater import auto_update_and_reexec
1485
1529
  auto_update_and_reexec("sdk.run_auto", sys.argv[1:])
1486
1530
 
1487
1531
  # Refuse to start a second loop — one background daemon owns the queue.
1488
- pid = _daemon_running()
1489
- if pid:
1490
- print(f"agentos-auto is already running in the background (pid {pid}).")
1491
- print(f" logs: {_LOG_FILE}")
1492
- print(" stop: agentos-auto stop")
1493
- return
1532
+ # (The re-exec'd daemon IS that loop; its own live PID would trip this.)
1533
+ if not resuming:
1534
+ pid = _daemon_running()
1535
+ if pid:
1536
+ print(f"agentos-auto is already running in the background (pid {pid}).")
1537
+ print(f" logs: {_LOG_FILE}")
1538
+ print(" stop: agentos-auto stop")
1539
+ return
1494
1540
 
1495
1541
  workspace_root = _parse_workspace_root(argv)
1496
1542
 
@@ -1513,7 +1559,7 @@ def main() -> None:
1513
1559
  # the loop — it runs until `agentos-auto stop`. `--foreground`/`-f` keeps it
1514
1560
  # attached (useful for debugging). On platforms without fork we stay attached.
1515
1561
  foreground = "--foreground" in argv or "-f" in argv
1516
- if not foreground and hasattr(os, "fork"):
1562
+ if not foreground and not resuming and hasattr(os, "fork"):
1517
1563
  if stories_only:
1518
1564
  target = (f" — stories→tasks: {', '.join(only_keys)}" if only_keys
1519
1565
  else " — converting all To Do Stories → Tasks")
@@ -1527,7 +1573,7 @@ def main() -> None:
1527
1573
  print(" status: agentos-auto status")
1528
1574
  print(" stop: agentos-auto stop")
1529
1575
  if not foreground and _daemonize():
1530
- # detached daemon child only
1576
+ # detached daemon child only (on macOS, the re-exec'd process)
1531
1577
  _run_loop(workspace_root, only_keys, stories_only, agent_only)
1532
1578
  return
1533
1579
  if not foreground:
@@ -158,6 +158,37 @@ class TestParser:
158
158
  c = M.EsloggerCollector(str(log), parser=_parser(), state_file=tmp_path / "st.json")
159
159
  assert c._state == tmp_path / "st.json"
160
160
 
161
+ def test_tail_loop_records_events_and_does_not_crash_on_an_idle_poll(
162
+ self, tmp_path, monkeypatch
163
+ ):
164
+ """Regression: EsloggerCollector reuses AuditdCollector's tail loop, which calls
165
+ ``parser.flush_older_than`` on EVERY poll. EsloggerParser was missing it, so the
166
+ collector thread died with AttributeError on its first (idle) poll — the tail
167
+ starts at EOF, so the first poll reads nothing and goes straight to that call —
168
+ and not a single event was ever recorded (SDK v1.6.52 on the Mac VPS)."""
169
+ from sdk.activity.collectors import linux_auditd as L
170
+
171
+ log = tmp_path / "es.ndjson"
172
+ log.write_text(_exec(1, ["old"], tty="/dev/ttys001") + "\n") # pre-install: not backfilled
173
+ collector = M.EsloggerCollector(
174
+ str(log), parser=_parser(), state_file=tmp_path / "st.json", poll_s=0,
175
+ )
176
+ calls = {"n": 0}
177
+
178
+ def fake_sleep(_):
179
+ calls["n"] += 1
180
+ if calls["n"] == 1: # new activity lands after the first, empty poll
181
+ with log.open("a") as fh:
182
+ fh.write(_exec(2, ["whoami"], tty="/dev/ttys001") + "\n")
183
+ else:
184
+ collector.stop()
185
+
186
+ monkeypatch.setattr(L.time, "sleep", fake_sleep)
187
+ collector.run() # before the fix: raises AttributeError out of the loop
188
+
189
+ _, events = _queued()
190
+ assert [e["command"] for e in events] == ["whoami"]
191
+
161
192
 
162
193
  # ─────────────────────────────── setup ───────────────────────────────
163
194
 
@@ -27,6 +27,7 @@ from __future__ import annotations
27
27
 
28
28
  import asyncio
29
29
  import os
30
+ import sys
30
31
 
31
32
  import pytest
32
33
 
@@ -596,6 +597,91 @@ def test_the_running_daemon_guard_still_wins(monkeypatch, tmp_path, capsys):
596
597
  assert "already running in the background (pid 4242)" in capsys.readouterr().out
597
598
 
598
599
 
600
+ # ── macOS fork-safety: the daemon re-execs a fresh interpreter ──────────────
601
+ # A bare double-fork() with no exec() leaves Apple's Network.framework / os_log
602
+ # half-initialised in the child, so the first getaddrinfo on the asyncio resolver
603
+ # thread SIGSEGVs ("crashed on child side of fork pre-exec"). _daemonize() re-execs
604
+ # the interpreter after detaching; main() re-enters with _DAEMONIZED_ENV set and
605
+ # must treat THIS process as the loop owner — not fork again, not trip its own guard.
606
+
607
+ def test_the_reexec_daemon_is_the_loop_not_a_second_one(monkeypatch, tmp_path, capsys):
608
+ """The re-exec'd daemon keeps the PID written before the exec, so _daemon_running()
609
+ would report IT as 'already running' and the loop would exit on its own guard.
610
+ With the marker set that guard — and the update and the banner — must be skipped."""
611
+ calls = _stub_main(monkeypatch, tmp_path, resolved=None, forkable=True)
612
+ monkeypatch.setattr(run_auto, "_daemon_running", lambda: os.getpid()) # our own live PID
613
+ monkeypatch.setenv(run_auto._DAEMONIZED_ENV, "1")
614
+ monkeypatch.setattr("sys.argv", ["agentos-auto"]) # no --no-update either
615
+
616
+ run_auto.main()
617
+
618
+ assert calls["run_loop"] == [(None, None, False, False)] # the loop actually ran
619
+ out = capsys.readouterr().out
620
+ assert "already running" not in out and "started in the background" not in out
621
+
622
+
623
+ def test_daemonize_marker_short_circuits_without_forking(monkeypatch, tmp_path):
624
+ """Re-entered with the marker, _daemonize() must report 'I am the daemon' WITHOUT
625
+ forking (setsid/fds/PID already survive the exec) and clear the marker so the
626
+ children the loop later spawns don't inherit it and skip their own daemonizing."""
627
+ monkeypatch.setattr(run_auto, "_LOG_FILE", tmp_path / "auto.log")
628
+ monkeypatch.setenv(run_auto._DAEMONIZED_ENV, "1")
629
+ monkeypatch.setattr(os, "fork", lambda: (_ for _ in ()).throw(AssertionError("forked")))
630
+
631
+ saved_out, saved_err = sys.stdout, sys.stderr
632
+ try:
633
+ assert run_auto._daemonize() is True
634
+ finally:
635
+ sys.stdout.close()
636
+ sys.stdout, sys.stderr = saved_out, saved_err
637
+
638
+ assert run_auto._DAEMONIZED_ENV not in os.environ # not leaked to spawned children
639
+
640
+
641
+ class _ExecvReached(BaseException):
642
+ """Marks that os.execv was called — a BaseException so _daemonize's
643
+ ``except Exception`` fallback does not swallow it."""
644
+
645
+
646
+ def test_macos_daemonize_reexecs_a_fresh_interpreter(monkeypatch, tmp_path):
647
+ """On macOS the detached child does not run in-process: it re-execs the
648
+ interpreter (clean Network.framework state) with the marker set, carrying the
649
+ same argv so main() resumes the loop."""
650
+ monkeypatch.setattr(sys, "platform", "darwin")
651
+ monkeypatch.setattr(run_auto, "_LOG_FILE", tmp_path / "auto.log")
652
+ monkeypatch.setattr(run_auto, "_AGENTOS_DIR", tmp_path)
653
+ monkeypatch.setattr(run_auto, "_write_pid", lambda: None)
654
+ monkeypatch.delenv(run_auto._DAEMONIZED_ENV, raising=False)
655
+ monkeypatch.setattr(os, "fork", lambda: 0) # always the child
656
+ monkeypatch.setattr(os, "setsid", lambda: None)
657
+ monkeypatch.setattr(os, "open", lambda *a, **k: 3)
658
+ monkeypatch.setattr(os, "dup2", lambda *a: None)
659
+ monkeypatch.setattr(os, "close", lambda *a: None)
660
+ # Under pytest the captured std streams have no real fileno(); the fd shuffle
661
+ # is already a no-op above, so hand _daemonize plain stand-ins to flush/number.
662
+ fake_std = type("S", (), {"flush": lambda self: None, "fileno": lambda self: 0})()
663
+ monkeypatch.setattr(sys, "stdin", fake_std)
664
+ monkeypatch.setattr(sys, "stdout", fake_std)
665
+ monkeypatch.setattr(sys, "stderr", fake_std)
666
+ monkeypatch.setattr("sys.argv", ["agentos-auto", "ticket", "BBW-9"])
667
+
668
+ seen = {}
669
+
670
+ def fake_execv(exe, argv):
671
+ seen["exe"], seen["argv"] = exe, argv
672
+ seen["marker"] = os.environ.get(run_auto._DAEMONIZED_ENV)
673
+ raise _ExecvReached
674
+
675
+ monkeypatch.setattr(os, "execv", fake_execv)
676
+
677
+ with pytest.raises(_ExecvReached):
678
+ run_auto._daemonize()
679
+
680
+ assert seen["exe"] == sys.executable
681
+ assert seen["argv"] == [sys.executable, "agentos-auto", "ticket", "BBW-9"]
682
+ assert seen["marker"] == "1" # the fresh interpreter learns it is the daemon
683
+
684
+
599
685
  def test_the_docstring_documents_the_flag_and_the_suffix():
600
686
  doc = run_auto.__doc__ or ""
601
687
  assert "--agent-only" in doc # Options block