devops-bot-sdk 1.6.64__tar.gz → 1.6.68__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 (230) hide show
  1. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
  4. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/models.py +52 -0
  6. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/redact.py +4 -1
  7. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/shipper.py +28 -8
  8. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/spool.py +19 -5
  9. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/service.py +94 -22
  10. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_activity_core.py +1 -1
  11. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_activity_pipeline.py +61 -0
  12. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_update_timer.py +2 -0
  13. devops_bot_sdk-1.6.68/tests/test_windows_task_paths.py +172 -0
  14. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/README.md +0 -0
  15. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  16. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  17. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/requires.txt +0 -0
  18. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  19. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/pyproject.toml +0 -0
  20. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/__init__.py +0 -0
  21. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/classifier.py +0 -0
  22. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/collectors/__init__.py +0 -0
  23. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/collectors/common.py +0 -0
  24. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/collectors/linux_auditd.py +0 -0
  25. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/collectors/macos_eslogger.py +0 -0
  26. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/collectors/windows_eventlog.py +0 -0
  27. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/os_setup.py +0 -0
  28. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/recorder.py +0 -0
  29. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/activity/settings.py +0 -0
  30. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agentd.py +0 -0
  31. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/__init__.py +0 -0
  32. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/activity.py +0 -0
  33. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/builder.py +0 -0
  34. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/chat.py +0 -0
  35. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/connectors.py +0 -0
  36. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/__init__.py +0 -0
  37. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/claude_code.py +0 -0
  38. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/http.py +0 -0
  39. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/mcp.py +0 -0
  40. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/n8n.py +0 -0
  41. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/engines/shell.py +0 -0
  42. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/integrations.py +0 -0
  43. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/inventory.py +0 -0
  44. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/library/__init__.py +0 -0
  45. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/manifest.py +0 -0
  46. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/prompt_studio.py +0 -0
  47. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/prompt_template.py +0 -0
  48. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/prompt_vars.py +0 -0
  49. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/run_log.py +0 -0
  50. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/schedules.py +0 -0
  51. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/secrets.py +0 -0
  52. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/ticket_run.py +0 -0
  53. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/usage.py +0 -0
  54. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/agents/varstore.py +0 -0
  55. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/browser_profile.py +0 -0
  56. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/cli.py +0 -0
  57. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/client.py +0 -0
  58. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/collectors/__init__.py +0 -0
  59. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/collectors/files.py +0 -0
  60. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/collectors/process.py +0 -0
  61. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/collectors/screenshot.py +0 -0
  62. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/config.py +0 -0
  63. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/crucial.py +0 -0
  64. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/custom_agents.py +0 -0
  65. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/departments.py +0 -0
  66. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/__init__.py +0 -0
  67. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/assets.py +0 -0
  68. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/bootstrap.py +0 -0
  69. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/browser.py +0 -0
  70. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/compare.py +0 -0
  71. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/design_verify/loop.py +0 -0
  72. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/estimator.py +0 -0
  73. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/exceptions.py +0 -0
  74. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/firewall.py +0 -0
  75. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/formats.py +0 -0
  76. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/git_ops.py +0 -0
  77. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/github_fetch.py +0 -0
  78. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/github_ingest.py +0 -0
  79. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/graph_recon.py +0 -0
  80. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/graphify.py +0 -0
  81. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/hooks/__init__.py +0 -0
  82. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/hooks/crucial_guard.py +0 -0
  83. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/intake.py +0 -0
  84. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ipc/__init__.py +0 -0
  85. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ipc/electron_bridge.py +0 -0
  86. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ipc/handlers.py +0 -0
  87. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/journal.py +0 -0
  88. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/local_exec.py +0 -0
  89. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/memory_guard.py +0 -0
  90. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/models/__init__.py +0 -0
  91. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/models/envelope.py +0 -0
  92. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/models/requests.py +0 -0
  93. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/models/responses.py +0 -0
  94. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/models/snapshots.py +0 -0
  95. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/phase_timer.py +0 -0
  96. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/plan_run.py +0 -0
  97. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/platform_compat.py +0 -0
  98. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/prd_progress.py +0 -0
  99. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/proof_readers.py +0 -0
  100. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/providers.py +0 -0
  101. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/py.typed +0 -0
  102. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/resume.py +0 -0
  103. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/router.py +0 -0
  104. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/run_auto.py +0 -0
  105. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/scheduler.py +0 -0
  106. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/self_report.py +0 -0
  107. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/self_report_local.py +0 -0
  108. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/sse.py +0 -0
  109. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/swagger.py +0 -0
  110. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/task_file.py +0 -0
  111. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/task_types.py +0 -0
  112. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/test.py +0 -0
  113. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/test_pipeline.py +0 -0
  114. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ticket_fields.py +0 -0
  115. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/tickets.py +0 -0
  116. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/tiers/__init__.py +0 -0
  117. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/tiers/mini_agent.py +0 -0
  118. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/__init__.py +0 -0
  119. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/api.py +0 -0
  120. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/dashboard.py +0 -0
  121. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/health.py +0 -0
  122. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/ip_allowlist.py +0 -0
  123. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/jobs.py +0 -0
  124. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/models.py +0 -0
  125. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/runner.py +0 -0
  126. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/security.py +0 -0
  127. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/server.py +0 -0
  128. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/session.py +0 -0
  129. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/setup_manager.py +0 -0
  130. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/agents.html +0 -0
  131. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/app.js +0 -0
  132. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/chat.html +0 -0
  133. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/custom-agents.html +0 -0
  134. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/default-agents.html +0 -0
  135. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/index.html +0 -0
  136. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/report.html +0 -0
  137. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/runs.html +0 -0
  138. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/schedules.html +0 -0
  139. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/setup.html +0 -0
  140. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/styles.css +0 -0
  141. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/static/tickets.html +0 -0
  142. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/ui/swagger.py +0 -0
  143. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/updater.py +0 -0
  144. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/url_access.py +0 -0
  145. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/vendor/aidraftsman-cli-0.1.0.tgz +0 -0
  146. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/vps_health.py +0 -0
  147. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/sdk/write_hook.py +0 -0
  148. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/setup.cfg +0 -0
  149. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_activity_collectors.py +0 -0
  150. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_activity_macos.py +0 -0
  151. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_activity_os_setup.py +0 -0
  152. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_activity.py +0 -0
  153. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_activity_tracking.py +0 -0
  154. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_chat_features.py +0 -0
  155. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_crud_cli.py +0 -0
  156. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_listing.py +0 -0
  157. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_prompt_async.py +0 -0
  158. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_prompt_models.py +0 -0
  159. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_run_ticket.py +0 -0
  160. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_schedules.py +0 -0
  161. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agent_workspace_settings.py +0 -0
  162. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_agents_connectors_builder.py +0 -0
  163. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_allowed_tools_config.py +0 -0
  164. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_auto_agent_dispatch.py +0 -0
  165. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_browser_profile.py +0 -0
  166. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_cheap_tier.py +0 -0
  167. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_client_jira_projects.py +0 -0
  168. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_config_token_diagnostics.py +0 -0
  169. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_configure_token_owner.py +0 -0
  170. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_console_encoding.py +0 -0
  171. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_crash_recovery.py +0 -0
  172. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_departments.py +0 -0
  173. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_done_transition.py +0 -0
  174. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_epic_decompose_run.py +0 -0
  175. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_epic_inherited_agent_id.py +0 -0
  176. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_epic_session_isolation.py +0 -0
  177. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_estimator.py +0 -0
  178. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_fallback_cli.py +0 -0
  179. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_firewall_open_on_update.py +0 -0
  180. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_formats.py +0 -0
  181. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_git_action.py +0 -0
  182. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_github_fetch.py +0 -0
  183. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_github_ingest.py +0 -0
  184. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_graph_recon.py +0 -0
  185. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_graphify_build.py +0 -0
  186. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_graphify_hook_install.py +0 -0
  187. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_health.py +0 -0
  188. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_intake_job_outcome.py +0 -0
  189. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_intake_plan_review.py +0 -0
  190. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_journal.py +0 -0
  191. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_local_exec_provider.py +0 -0
  192. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_mcp_route_contract.py +0 -0
  193. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_memory_guard.py +0 -0
  194. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_phase_timer.py +0 -0
  195. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_plan_new_command.py +0 -0
  196. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_plan_run_activity.py +0 -0
  197. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_pr_identity.py +0 -0
  198. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_prd_artifact.py +0 -0
  199. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_project_path_resolution.py +0 -0
  200. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_proof_readers.py +0 -0
  201. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_providers.py +0 -0
  202. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_recon_session_isolation.py +0 -0
  203. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_resume.py +0 -0
  204. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_router.py +0 -0
  205. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_run_auto_agent_only.py +0 -0
  206. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_run_outputs.py +0 -0
  207. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_run_verdict_is_honest.py +0 -0
  208. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_schedule_api.py +0 -0
  209. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_schedule_outputs_and_plans.py +0 -0
  210. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_scheduler.py +0 -0
  211. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_self_report.py +0 -0
  212. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_service_token_warning.py +0 -0
  213. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_stale_panel_restart.py +0 -0
  214. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_swagger_command.py +0 -0
  215. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_task_file_and_plan_run.py +0 -0
  216. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_task_file_format_doc.py +0 -0
  217. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ticket_cli.py +0 -0
  218. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ticket_fields.py +0 -0
  219. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ticket_run_resolve_by_id.py +0 -0
  220. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ticket_speed_fixes.py +0 -0
  221. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_tier_flag_parity.py +0 -0
  222. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ui_gate.py +0 -0
  223. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_ui_lifespan.py +0 -0
  224. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_updater_extras.py +0 -0
  225. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_updater_pep668.py +0 -0
  226. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_updater_reports_the_truth.py +0 -0
  227. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_url_access.py +0 -0
  228. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_vps_health.py +0 -0
  229. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/tests/test_windows_loop_identity.py +0 -0
  230. {devops_bot_sdk-1.6.64 → devops_bot_sdk-1.6.68}/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.64
3
+ Version: 1.6.68
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.64
3
+ Version: 1.6.68
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
@@ -224,4 +224,5 @@ tests/test_updater_reports_the_truth.py
224
224
  tests/test_url_access.py
225
225
  tests/test_vps_health.py
226
226
  tests/test_windows_loop_identity.py
227
+ tests/test_windows_task_paths.py
227
228
  tests/test_write_hook.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.64
3
+ Version: 1.6.68
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.64" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.68" # 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
@@ -16,6 +16,7 @@ retried at any hop is stored once.
16
16
  """
17
17
  from __future__ import annotations
18
18
 
19
+ import json
19
20
  import os
20
21
  import sys
21
22
  import time
@@ -140,6 +141,57 @@ def _wire(obj: Any) -> dict[str, Any]:
140
141
  return out
141
142
 
142
143
 
144
+ #: String caps the relay validates (devops-bot-ai ``modules/vps_activity/schemas.py``).
145
+ #: A value over its cap is a 422, and the shipper drops a row the relay refuses — so
146
+ #: one over-long path or command line used to cost the whole event. Clamp instead.
147
+ WIRE_MAX_CHARS: dict[str, dict[str, int]] = {
148
+ "event": {
149
+ "action": 100,
150
+ "osUser": 255,
151
+ "processName": 255,
152
+ "command": 20_000,
153
+ "target": 4096,
154
+ "agentRunId": 128,
155
+ "sessionKey": 128,
156
+ },
157
+ "session": {
158
+ "osUser": 255,
159
+ "remoteIp": 45,
160
+ "agentRunId": 128,
161
+ "initiatedByKey": 128,
162
+ },
163
+ }
164
+ _CLAMP_MARK = "…"
165
+ #: Free-form dicts the relay does not cap. Bounded here so that, with the string caps
166
+ #: above, no single row can outgrow a request (2 MB gzipped / 10 MB raw) — a row that
167
+ #: could would be 413'd on its own and dropped.
168
+ WIRE_DICT_FIELDS = ("metadata", "actorReasons")
169
+ MAX_DICT_FIELD_BYTES = 64 * 1024
170
+
171
+
172
+ def fit_wire_limits(kind: str, wire: dict[str, Any]) -> dict[str, Any]:
173
+ """``wire`` with every over-long field shortened to fit the relay's limits."""
174
+ out = wire
175
+
176
+ def _set(name: str, value: Any) -> None:
177
+ nonlocal out
178
+ if out is wire:
179
+ out = dict(wire)
180
+ out[name] = value
181
+
182
+ for name, cap in WIRE_MAX_CHARS.get(kind, {}).items():
183
+ value = wire.get(name)
184
+ if isinstance(value, str) and len(value) > cap:
185
+ _set(name, value[: cap - len(_CLAMP_MARK)] + _CLAMP_MARK)
186
+ for name in WIRE_DICT_FIELDS:
187
+ value = wire.get(name)
188
+ if isinstance(value, dict):
189
+ size = len(json.dumps(value, separators=(",", ":"), default=str))
190
+ if size > MAX_DICT_FIELD_BYTES:
191
+ _set(name, {"truncated": True, "originalBytes": size})
192
+ return out
193
+
194
+
143
195
  # ── models ───────────────────────────────────────────────────────────────────
144
196
 
145
197
 
@@ -72,6 +72,7 @@ _PATTERNS: list[tuple[re.Pattern[str], str]] = [
72
72
 
73
73
  #: Hard cap so a pathological command line can't bloat the spool.
74
74
  MAX_COMMAND_CHARS = 20_000
75
+ TRUNCATED = "…[truncated]"
75
76
 
76
77
 
77
78
  def redact_command(command: str | None) -> str | None:
@@ -82,5 +83,7 @@ def redact_command(command: str | None) -> str | None:
82
83
  for pattern, repl in _PATTERNS:
83
84
  out = pattern.sub(repl, out)
84
85
  if len(out) > MAX_COMMAND_CHARS:
85
- out = out[:MAX_COMMAND_CHARS] + "…[truncated]"
86
+ # The marker counts toward the cap: the relay 422s anything longer, and the
87
+ # shipper drops a refused event rather than shipping it truncated.
88
+ out = out[: MAX_COMMAND_CHARS - len(TRUNCATED)] + TRUNCATED
86
89
  return out
@@ -7,8 +7,9 @@ Delivery rules — a row leaves the spool ONLY when the relay answered 202 (or i
7
7
  provably undeliverable):
8
8
 
9
9
  2xx delivered → delete the chunk
10
- network error, 5xx, 408, 429 retry (short backoff); if still failing, keep
10
+ network error, 5xx, 408 retry (short backoff); if still failing, keep
11
11
  everything and try again at the next flush
12
+ 429 wait out the relay's 60 s window, then retry
12
13
  401, 426 token / SDK version needs an operator → keep, stop
13
14
  this flush (retrying in a tight loop helps nobody)
14
15
  any other 4xx the relay READ the chunk and refused it. Dropping all
@@ -16,8 +17,9 @@ provably undeliverable):
16
17
  so the chunk is BISECTED until the offending row is
17
18
  isolated; only that single row is dropped.
18
19
 
19
- A flush drains the whole spool, chunk by chunk (≤500 events / ≤1 MB each), because
20
- five minutes of full OS auditing is routinely thousands of events.
20
+ A flush drains the whole spool, chunk by chunk (≤500 events / ≤4 MB each), paced under
21
+ the relay's 60-requests-a-minute budget, because five minutes of full OS auditing is
22
+ routinely thousands of events.
21
23
  """
22
24
  from __future__ import annotations
23
25
 
@@ -36,9 +38,18 @@ logger = logging.getLogger("sdk.activity.shipper")
36
38
  RETRY_DELAYS_S = (2.0, 8.0)
37
39
  #: Safety cap on chunks per flush so one flush can't run away.
38
40
  MAX_CHUNKS_PER_FLUSH = 200
41
+ #: The relay allows 60 batches per minute per token. Firing chunks back-to-back blew
42
+ #: through that after ~60 chunks; the 2 s / 8 s retries cannot outlast a 60 s window,
43
+ #: so the flush gave up and a busy host never caught up. Pace below the budget...
44
+ MIN_POST_GAP_S = 1.1
45
+ #: ...and if a 429 still lands (another process on the same token), wait the window
46
+ #: out rather than abandoning the flush.
47
+ RATE_LIMIT_WAIT_S = 61.0
48
+ MAX_RATE_LIMIT_WAITS = 3
39
49
 
40
50
  _NEEDS_OPERATOR = {401, 426}
41
51
  _TRANSIENT_4XX = {408, 429}
52
+ _RATE_LIMITED = 429
42
53
 
43
54
  Sleep = Callable[[float], Awaitable[Any]]
44
55
 
@@ -73,20 +84,27 @@ def _verdict(status: int | None) -> str:
73
84
 
74
85
  async def _attempt(sender: Sender, chunk: Chunk, sleep: Sleep) -> str:
75
86
  """One chunk with short in-flush retries → ``ok`` / ``drop`` / ``stop`` / ``later``."""
76
- for delay in (0.0, *RETRY_DELAYS_S):
77
- if delay:
78
- await sleep(delay)
87
+ retry_delays = list(RETRY_DELAYS_S)
88
+ rate_limit_waits = 0
89
+ while True:
79
90
  try:
80
91
  status: int | None = await sender.post_vps_activity_batch(chunk.to_wire())
81
92
  except Exception as exc: # noqa: BLE001 — httpx errors, DNS, TLS, ...
82
93
  logger.warning("activity: ship failed (%s); will retry", exc)
83
94
  status = None
95
+ if status == _RATE_LIMITED and rate_limit_waits < MAX_RATE_LIMIT_WAITS:
96
+ rate_limit_waits += 1
97
+ logger.info("activity: relay rate-limited; waiting %.0fs", RATE_LIMIT_WAIT_S)
98
+ await sleep(RATE_LIMIT_WAIT_S)
99
+ continue
84
100
  verdict = _verdict(status)
85
101
  if verdict != "retry":
86
102
  if verdict == "stop":
87
103
  logger.warning("activity: relay answered %s — needs operator attention", status)
88
104
  return verdict
89
- return "later"
105
+ if not retry_delays:
106
+ return "later"
107
+ await sleep(retry_delays.pop(0))
90
108
 
91
109
 
92
110
  async def _deliver(
@@ -135,10 +153,12 @@ async def flush_once(
135
153
  from sdk.client import BackendClient
136
154
 
137
155
  sender = BackendClient.from_config()
138
- for _ in range(max_chunks):
156
+ for i in range(max_chunks):
139
157
  chunk = spool.next_chunk()
140
158
  if not chunk:
141
159
  break
160
+ if i:
161
+ await sleep(MIN_POST_GAP_S)
142
162
  if not await _deliver(spool, sender, chunk, result, sleep):
143
163
  break
144
164
  else:
@@ -23,14 +23,21 @@ from dataclasses import dataclass, field
23
23
  from pathlib import Path
24
24
  from typing import Any
25
25
 
26
+ from sdk.activity.models import fit_wire_limits
27
+
26
28
  logger = logging.getLogger("sdk.activity.spool")
27
29
 
28
30
  #: Rows kept at most. ~200k events is days of a busy host; beyond it, shed oldest.
29
31
  DEFAULT_MAX_ROWS = 200_000
30
- #: Wire limits per request — the relay and the gateway enforce the same cap.
32
+ #: Wire limits per request — the relay and the gateway enforce the same row cap.
31
33
  MAX_EVENTS_PER_CHUNK = 500
32
34
  MAX_SESSIONS_PER_CHUNK = 500
33
- MAX_CHUNK_BYTES = 1_000_000
35
+ #: Uncompressed JSON per request. The relay takes 10 MB decompressed / 2 MB gzipped;
36
+ #: audit JSON gzips well past 2:1, so 4 MB stays under both. A 1 MB cap meant a host
37
+ #: with long command lines sent small chunks, ran into the relay's per-minute request
38
+ #: budget and fell behind until the spool shed its oldest rows. A chunk that still
39
+ #: comes back 413 is bisected by the shipper, so this is a target, not a cliff.
40
+ MAX_CHUNK_BYTES = 4_000_000
34
41
 
35
42
  _SCHEMA = """
36
43
  CREATE TABLE IF NOT EXISTS spool (
@@ -110,7 +117,10 @@ class Spool:
110
117
  with self._lock:
111
118
  self._db.execute(
112
119
  "INSERT OR IGNORE INTO spool (kind, key, payload) VALUES ('event', ?, ?)",
113
- (wire["eventId"], json.dumps(wire, separators=(",", ":"))),
120
+ (
121
+ wire["eventId"],
122
+ json.dumps(fit_wire_limits("event", wire), separators=(",", ":")),
123
+ ),
114
124
  )
115
125
  self._trim()
116
126
 
@@ -127,7 +137,10 @@ class Spool:
127
137
  "INSERT INTO spool (kind, key, payload) VALUES ('session', ?, ?) "
128
138
  "ON CONFLICT (kind, key) DO UPDATE SET "
129
139
  "payload = excluded.payload, rev = rev + 1",
130
- (wire["externalKey"], json.dumps(wire, separators=(",", ":"))),
140
+ (
141
+ wire["externalKey"],
142
+ json.dumps(fit_wire_limits("session", wire), separators=(",", ":")),
143
+ ),
131
144
  )
132
145
  self._trim()
133
146
 
@@ -176,7 +189,8 @@ class Spool:
176
189
  break
177
190
  if chunk.rows and size + len(payload) > max_bytes:
178
191
  break
179
- chunk.rows.append(Row(seq, rev, kind, json.loads(payload)))
192
+ # Re-fit on the way out too: rows queued by an older SDK predate the clamp.
193
+ chunk.rows.append(Row(seq, rev, kind, fit_wire_limits(kind, json.loads(payload))))
180
194
  n_events += kind == "event"
181
195
  n_sessions += kind == "session"
182
196
  size += len(payload)
@@ -33,25 +33,65 @@ WIN_TASK = "AgentOS Control Panel" # Windows scheduled-task name
33
33
  WIN_UPDATE_TASK = "AgentOS Self-Update" # Windows counterpart of agentos-update.timer
34
34
 
35
35
 
36
+ def _script_dirs() -> list[Path]:
37
+ """Where pip puts console scripts for THIS interpreter, most specific first.
38
+
39
+ Next to the interpreter covers a venv (``bin/`` / ``Scripts\\`` holds both
40
+ ``python`` and the scripts). It does NOT cover a regular Windows install, where
41
+ ``python.exe`` sits in the install root and the scripts in its ``Scripts\\``
42
+ subfolder — ``sysconfig``'s ``scripts`` path is the one that knows. The user
43
+ scheme covers a ``pip install --user``.
44
+ """
45
+ import sysconfig
46
+
47
+ dirs = [Path(sys.executable).parent]
48
+ for args in (("scripts",), ("scripts", f"{os.name}_user")):
49
+ try:
50
+ p = sysconfig.get_path(*args)
51
+ except KeyError: # scheme not defined on this platform
52
+ continue
53
+ if p and Path(p) not in dirs:
54
+ dirs.append(Path(p))
55
+ return dirs
56
+
57
+
58
+ def _console_script(base: str) -> str | None:
59
+ """ABSOLUTE path to this SDK's ``base`` console script, or ``None`` if not found."""
60
+ name = f"{base}.exe" if IS_WINDOWS else base
61
+ for d in _script_dirs():
62
+ cand = d / name
63
+ if cand.exists():
64
+ return str(cand)
65
+ found = shutil.which(name)
66
+ return str(Path(found).resolve()) if found else None
67
+
68
+
36
69
  def _exe() -> str:
37
70
  """ABSOLUTE path to the ``agentos-ui`` console script.
38
71
 
39
- Must be absolute: a service manager (systemd/launchd) does NOT search the
40
- user's PATH for ExecStart, so a bare ``agentos-ui`` fails with 203/EXEC when
41
- the script lives somewhere like ``~/.local/bin`` (a ``pip --user`` install) —
42
- the common case on a VPS. Resolve, in order:
43
- 1. next to the running interpreter (venv installs),
44
- 2. on the current PATH (finds ``~/.local/bin`` at install time),
45
- 3. bare name as a last resort (kept only so install never hard-fails).
72
+ Must be absolute: a service manager (systemd/launchd/Task Scheduler) does NOT
73
+ search the user's PATH, so a bare ``agentos-ui`` fails with 203/EXEC when the
74
+ script lives somewhere like ``~/.local/bin`` (a ``pip --user`` install) — the
75
+ common case on a VPS — and with 0x80070002 (file not found) on Windows, where
76
+ it lives in ``<python>\\Scripts``. Resolved via :func:`_console_script`; the
77
+ bare name is a last resort kept so a systemd install never hard-fails. The
78
+ Windows installers refuse it (see :func:`_require_absolute`).
46
79
  """
47
- name = "agentos-ui.exe" if IS_WINDOWS else "agentos-ui"
48
- cand = Path(sys.executable).parent / name
49
- if cand.exists():
50
- return str(cand)
51
- found = shutil.which(name)
52
- if found:
53
- return str(Path(found).resolve())
54
- return name
80
+ return _console_script("agentos-ui") or ("agentos-ui.exe" if IS_WINDOWS else "agentos-ui")
81
+
82
+
83
+ def _require_absolute(exe: str, task: str) -> str | None:
84
+ """Refusal message when ``exe`` is not an absolute path, else ``None``.
85
+
86
+ A scheduled task with a bare program name is created without complaint and then
87
+ never starts — Last Result 0x80070002, no panel, no activity trail, nothing on
88
+ screen. Refusing here turns that silence into a message the updater prints.
89
+ """
90
+ if Path(exe).is_absolute():
91
+ return None
92
+ return (f"not creating task '{task}': could not find {exe} for this Python "
93
+ f"({sys.executable}). Reinstall the SDK with that interpreter and re-run "
94
+ f"`agentos-auto update`.")
55
95
 
56
96
 
57
97
  def _run(argv: list[str]) -> tuple[int, str]:
@@ -169,10 +209,7 @@ def _auto_exe() -> str:
169
209
  minimal environment, and picking up a different venv's console script is how
170
210
  an updater ends up upgrading the wrong installation.
171
211
  """
172
- candidate = Path(sys.executable).parent / ("agentos-auto.exe" if IS_WINDOWS else "agentos-auto")
173
- if candidate.exists():
174
- return str(candidate)
175
- return shutil.which("agentos-auto") or "agentos-auto"
212
+ return _console_script("agentos-auto") or ("agentos-auto.exe" if IS_WINDOWS else "agentos-auto")
176
213
 
177
214
 
178
215
  def update_timer_installed() -> bool:
@@ -374,7 +411,11 @@ def _uninstall_launchd() -> tuple[bool, str]:
374
411
  # ── Windows (Scheduled Task) ────────────────────────────────────────────────
375
412
 
376
413
  def _install_schtask(host: str, port: int) -> tuple[bool, str]:
377
- tr = f'"{_exe()}" --host {host} --port {port} --no-browser'
414
+ exe = _exe()
415
+ refusal = _require_absolute(exe, WIN_TASK)
416
+ if refusal:
417
+ return False, refusal
418
+ tr = f'"{exe}" --host {host} --port {port} --no-browser'
378
419
  rc, out = _run(["schtasks", "/Create", "/TN", WIN_TASK, "/TR", tr,
379
420
  "/SC", "ONLOGON", "/RL", "HIGHEST", "/F"])
380
421
  if rc != 0:
@@ -440,7 +481,11 @@ def _install_update_schtask() -> tuple[bool, str]:
440
481
  """
441
482
  import tempfile
442
483
 
443
- xml = _update_task_xml(_auto_exe(), str(Path.home()))
484
+ exe = _auto_exe()
485
+ refusal = _require_absolute(exe, WIN_UPDATE_TASK)
486
+ if refusal:
487
+ return False, refusal
488
+ xml = _update_task_xml(exe, str(Path.home()))
444
489
  fd, path = tempfile.mkstemp(suffix=".xml")
445
490
  try:
446
491
  with os.fdopen(fd, "w", encoding="utf-16") as fh: # schtasks /XML wants UTF-16
@@ -459,6 +504,29 @@ def _install_update_schtask() -> tuple[bool, str]:
459
504
  f"daily, randomized, catches up after downtime")
460
505
 
461
506
 
507
+ def _schtask_command(task: str) -> str | None:
508
+ """The program ``task`` runs, from its XML definition; ``None`` if unreadable."""
509
+ import html
510
+
511
+ rc, out = _run(["schtasks", "/Query", "/TN", task, "/XML"])
512
+ if rc != 0:
513
+ return None
514
+ m = re.search(r"<Command>(.*?)</Command>", out, re.S)
515
+ return html.unescape(m.group(1)).strip().strip('"') if m else None
516
+
517
+
518
+ def _schtask_points_nowhere(task: str) -> bool:
519
+ """True when ``task`` exists but runs a program that is not an existing file.
520
+
521
+ Boxes installed before :func:`_script_dirs` existed have a panel task whose
522
+ program is the bare ``agentos-ui.exe``. It shows "Ready", never runs, and the
523
+ ordinary update path ("installed + unchanged") only ever re-runs it — so without
524
+ this check those boxes would stay broken through every update.
525
+ """
526
+ cmd = _schtask_command(task)
527
+ return cmd is not None and not (Path(cmd).is_absolute() and Path(cmd).is_file())
528
+
529
+
462
530
  def _uninstall_schtask() -> tuple[bool, str]:
463
531
  _run(["schtasks", "/Delete", "/TN", WIN_UPDATE_TASK, "/F"])
464
532
  _run(["schtasks", "/End", "/TN", WIN_TASK])
@@ -771,7 +839,7 @@ def ensure(restart_running: bool = False) -> tuple[bool, str]:
771
839
  if not is_installed():
772
840
  ok, msg = install() # create at the current privilege
773
841
  return ok, (f"service created — {msg}" if ok else f"could not create service: {msg}")
774
- if not update_timer_installed():
842
+ if not update_timer_installed() or (IS_WINDOWS and _schtask_points_nowhere(WIN_UPDATE_TASK)):
775
843
  _install_update_timer()
776
844
  if _needs_root_for_existing():
777
845
  return False, ("a system service is installed — run `sudo agentos-ui install` "
@@ -783,6 +851,10 @@ def ensure(restart_running: bool = False) -> tuple[bool, str]:
783
851
  if restart_running:
784
852
  rok, rmsg = refresh()
785
853
  return rok, (_with_token_warning(rmsg) if rok else rmsg)
854
+ if IS_WINDOWS and _schtask_points_nowhere(WIN_TASK):
855
+ rok, rmsg = refresh() # rebuilds the task with an absolute path
856
+ return rok, (_with_token_warning(f"panel task pointed at a missing program — "
857
+ f"rebuilt; {rmsg}") if rok else rmsg)
786
858
  if _is_active():
787
859
  stale = _restart_if_stale()
788
860
  if stale is not None:
@@ -176,7 +176,7 @@ class TestRedaction:
176
176
  def test_is_length_capped(self):
177
177
  out = redact_command("a" * (MAX_COMMAND_CHARS + 500))
178
178
  assert out.endswith("…[truncated]")
179
- assert len(out) < MAX_COMMAND_CHARS + 50
179
+ assert len(out) == MAX_COMMAND_CHARS # the relay 422s anything longer
180
180
 
181
181
 
182
182
  class TestClassifier:
@@ -66,6 +66,45 @@ class TestSpool:
66
66
  _sess("S1", actorType="HUMAN", endedAt="2026-10-09T12:00:00.000Z")
67
67
  ]
68
68
 
69
+ def test_over_long_fields_are_clamped_to_the_relay_caps_not_left_to_be_refused(self, spool):
70
+ from sdk.activity.models import WIRE_MAX_CHARS
71
+
72
+ spool.add_event(_ev("E1", command="c" * 30_000, target="/t" * 5000, processName="p" * 999))
73
+ spool.add_session(_sess("S1", osUser="u" * 999))
74
+ chunk = spool.next_chunk()
75
+ ev, sess = chunk.events[0], chunk.sessions[0]
76
+ for name in ("command", "target", "processName"):
77
+ assert len(ev[name]) == WIRE_MAX_CHARS["event"][name]
78
+ assert len(sess["osUser"]) == WIRE_MAX_CHARS["session"]["osUser"]
79
+ assert ev["action"] == "exec" # short fields untouched
80
+
81
+ def test_an_oversized_metadata_dict_is_replaced_so_the_row_still_ships(self, spool):
82
+ from sdk.activity.models import MAX_DICT_FIELD_BYTES
83
+
84
+ spool.add_event(_ev("E1", metadata={"blob": "x" * (MAX_DICT_FIELD_BYTES * 2)}))
85
+ spool.add_event(_ev("E2", metadata={"small": 1}))
86
+ events = spool.next_chunk().events
87
+ assert events[0]["metadata"]["truncated"] is True
88
+ assert events[0]["metadata"]["originalBytes"] > MAX_DICT_FIELD_BYTES
89
+ assert events[1]["metadata"] == {"small": 1}
90
+
91
+ def test_a_single_worst_case_row_fits_comfortably_in_one_request(self, spool):
92
+ from sdk.activity.models import MAX_DICT_FIELD_BYTES
93
+
94
+ spool.add_event(_ev(
95
+ "E1", command="c" * 99_999, target="t" * 99_999, processName="p" * 9_999,
96
+ metadata={"blob": "x" * (MAX_DICT_FIELD_BYTES * 10)},
97
+ ))
98
+ (row,) = spool.next_chunk().rows
99
+ import json as _json
100
+
101
+ assert len(_json.dumps(row.wire)) < 200_000 # vs 2 MB gzipped / 10 MB raw at the relay
102
+
103
+ def test_default_chunk_byte_cap_stays_under_the_relay_body_limit(self):
104
+ from sdk.activity import spool as spool_mod
105
+
106
+ assert 1_000_000 < spool_mod.MAX_CHUNK_BYTES < 10 * 1024 * 1024
107
+
69
108
  def test_rows_come_back_in_arrival_order_session_first(self, spool):
70
109
  spool.add_session(_sess("S1"))
71
110
  spool.add_event(_ev("E1"))
@@ -231,6 +270,28 @@ class TestShipper:
231
270
  result = _run(shipper.flush_once(spool, sender, sleep=Sleeps()))
232
271
  assert result.shipped_rows == 1
233
272
 
273
+ def test_a_429_waits_out_the_relay_window_instead_of_giving_up(self, spool):
274
+ spool.add_event(_ev("E1"))
275
+ sender = FakeSender(script=[429, 429, 429, 202])
276
+ sleeps = Sleeps()
277
+ result = _run(shipper.flush_once(spool, sender, sleep=sleeps))
278
+ assert result.shipped_rows == 1 and result.stopped is None
279
+ assert sleeps.delays == [shipper.RATE_LIMIT_WAIT_S] * 3
280
+
281
+ def test_endless_429s_keep_the_data_for_the_next_flush(self, spool):
282
+ spool.add_event(_ev("E1"))
283
+ sender = FakeSender(default=429)
284
+ result = _run(shipper.flush_once(spool, sender, sleep=Sleeps()))
285
+ assert result.stopped == "later" and spool.count() == 1
286
+
287
+ def test_chunks_are_paced_under_the_relay_request_budget(self, spool):
288
+ for i in range(1200):
289
+ spool.add_event(_ev(f"E{i:05d}"))
290
+ sleeps = Sleeps()
291
+ _run(shipper.flush_once(spool, FakeSender(), sleep=sleeps))
292
+ assert sleeps.delays == [shipper.MIN_POST_GAP_S] * 2 # 3 chunks, 2 gaps
293
+ assert shipper.MIN_POST_GAP_S * 60 > 60 # stays under 60 requests a minute
294
+
234
295
  @pytest.mark.parametrize("status", [401, 426])
235
296
  def test_token_or_version_problems_stop_without_retrying_or_dropping(self, spool, status):
236
297
  spool.add_event(_ev("E1"))
@@ -177,6 +177,7 @@ class TestWindowsUpdateTask:
177
177
 
178
178
  def test_creates_the_task_from_a_utf16_xml_file(self, monkeypatch):
179
179
  monkeypatch.setattr(service, "IS_WINDOWS", True)
180
+ monkeypatch.setattr(service, "_auto_exe", lambda: "/py/Scripts/agentos-auto.exe")
180
181
  seen = {}
181
182
 
182
183
  def fake_run(argv):
@@ -194,6 +195,7 @@ class TestWindowsUpdateTask:
194
195
 
195
196
  def test_a_failed_create_is_reported_not_raised(self, monkeypatch):
196
197
  monkeypatch.setattr(service, "IS_WINDOWS", True)
198
+ monkeypatch.setattr(service, "_auto_exe", lambda: "/py/Scripts/agentos-auto.exe")
197
199
  monkeypatch.setattr(service, "_run", lambda argv: (1, "Access is denied."))
198
200
  ok, msg = service._install_update_timer()
199
201
  assert ok is False and "Access is denied." in msg