devops-bot-sdk 1.7.5__tar.gz → 1.7.8__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 (270) hide show
  1. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/__init__.py +2 -2
  4. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/client.py +1 -1
  5. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/helpdesk_kb.md +9 -2
  6. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/log_colors.py +47 -6
  7. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/platform_compat.py +5 -4
  8. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/run_auto.py +2 -2
  9. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/api.py +62 -8
  10. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/app.js +67 -10
  11. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/styles.css +15 -0
  12. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agent_listing.py +88 -0
  13. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_log_colors_backlog.py +55 -0
  14. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/README.md +0 -0
  15. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
  16. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  17. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  18. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/devops_bot_sdk.egg-info/requires.txt +0 -0
  19. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  20. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/pyproject.toml +0 -0
  21. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/__init__.py +0 -0
  22. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/classifier.py +0 -0
  23. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/collectors/__init__.py +0 -0
  24. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/collectors/common.py +0 -0
  25. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/collectors/linux_auditd.py +0 -0
  26. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/collectors/macos_eslogger.py +0 -0
  27. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/collectors/windows_eventlog.py +0 -0
  28. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/models.py +0 -0
  29. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/os_setup.py +0 -0
  30. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/recorder.py +0 -0
  31. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/redact.py +0 -0
  32. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/settings.py +0 -0
  33. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/shipper.py +0 -0
  34. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/activity/spool.py +0 -0
  35. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agent_sharing.py +0 -0
  36. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agentd.py +0 -0
  37. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/__init__.py +0 -0
  38. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/activity.py +0 -0
  39. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/builder.py +0 -0
  40. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/chat.py +0 -0
  41. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/connectors.py +0 -0
  42. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/engines/__init__.py +0 -0
  43. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/engines/claude_code.py +0 -0
  44. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/engines/http.py +0 -0
  45. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/engines/mcp.py +0 -0
  46. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/engines/n8n.py +0 -0
  47. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/engines/shell.py +0 -0
  48. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/integrations.py +0 -0
  49. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/inventory.py +0 -0
  50. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/library/__init__.py +0 -0
  51. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/manifest.py +0 -0
  52. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/prompt_studio.py +0 -0
  53. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/prompt_template.py +0 -0
  54. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/prompt_vars.py +0 -0
  55. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/run_log.py +0 -0
  56. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/schedules.py +0 -0
  57. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/secrets.py +0 -0
  58. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/ticket_run.py +0 -0
  59. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/usage.py +0 -0
  60. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/agents/varstore.py +0 -0
  61. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/browser_profile.py +0 -0
  62. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/claude_usage.py +0 -0
  63. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/cli.py +0 -0
  64. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/collectors/__init__.py +0 -0
  65. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/collectors/files.py +0 -0
  66. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/collectors/process.py +0 -0
  67. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/collectors/screenshot.py +0 -0
  68. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/config.py +0 -0
  69. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/crucial.py +0 -0
  70. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/custom_agents.py +0 -0
  71. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/deferred_tickets.py +0 -0
  72. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/departments.py +0 -0
  73. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/design_verify/__init__.py +0 -0
  74. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/design_verify/assets.py +0 -0
  75. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/design_verify/bootstrap.py +0 -0
  76. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/design_verify/browser.py +0 -0
  77. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/design_verify/compare.py +0 -0
  78. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/design_verify/loop.py +0 -0
  79. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/estimator.py +0 -0
  80. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/exceptions.py +0 -0
  81. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/fallback_handoff.py +0 -0
  82. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/firewall.py +0 -0
  83. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/formats.py +0 -0
  84. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/git_ops.py +0 -0
  85. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/github_fetch.py +0 -0
  86. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/github_ingest.py +0 -0
  87. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/graph_recon.py +0 -0
  88. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/graphify.py +0 -0
  89. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/help_actions.py +0 -0
  90. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/helpdesk.py +0 -0
  91. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/hooks/__init__.py +0 -0
  92. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/hooks/crucial_guard.py +0 -0
  93. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/intake.py +0 -0
  94. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ipc/__init__.py +0 -0
  95. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ipc/electron_bridge.py +0 -0
  96. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ipc/handlers.py +0 -0
  97. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/journal.py +0 -0
  98. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/local_exec.py +0 -0
  99. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/memory_guard.py +0 -0
  100. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/model_policy.py +0 -0
  101. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/models/__init__.py +0 -0
  102. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/models/envelope.py +0 -0
  103. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/models/requests.py +0 -0
  104. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/models/responses.py +0 -0
  105. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/models/snapshots.py +0 -0
  106. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/phase_timer.py +0 -0
  107. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/plan_run.py +0 -0
  108. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/prd_progress.py +0 -0
  109. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/proof_readers.py +0 -0
  110. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/providers.py +0 -0
  111. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/py.typed +0 -0
  112. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/resume.py +0 -0
  113. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/router.py +0 -0
  114. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/schedule_tickets.py +0 -0
  115. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/scheduler.py +0 -0
  116. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/self_report.py +0 -0
  117. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/self_report_local.py +0 -0
  118. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/sse.py +0 -0
  119. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/swagger.py +0 -0
  120. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/task_file.py +0 -0
  121. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/task_types.py +0 -0
  122. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/test.py +0 -0
  123. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/test_pipeline.py +0 -0
  124. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ticket_board.py +0 -0
  125. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ticket_fields.py +0 -0
  126. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/tickets.py +0 -0
  127. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/tiers/__init__.py +0 -0
  128. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/tiers/mini_agent.py +0 -0
  129. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/__init__.py +0 -0
  130. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/dashboard.py +0 -0
  131. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/health.py +0 -0
  132. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/ip_allowlist.py +0 -0
  133. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/jobs.py +0 -0
  134. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/models.py +0 -0
  135. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/runner.py +0 -0
  136. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/security.py +0 -0
  137. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/server.py +0 -0
  138. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/service.py +0 -0
  139. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/session.py +0 -0
  140. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/setup_manager.py +0 -0
  141. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/agents.html +0 -0
  142. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/chat.html +0 -0
  143. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/custom-agents.html +0 -0
  144. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/default-agents.html +0 -0
  145. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/help.html +0 -0
  146. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/index.html +0 -0
  147. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/report.html +0 -0
  148. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/runs.html +0 -0
  149. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/schedules.html +0 -0
  150. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/setup.html +0 -0
  151. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/static/tickets.html +0 -0
  152. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/ui/swagger.py +0 -0
  153. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/updater.py +0 -0
  154. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/url_access.py +0 -0
  155. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/vps_health.py +0 -0
  156. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/vps_identity.py +0 -0
  157. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/sdk/write_hook.py +0 -0
  158. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/setup.cfg +0 -0
  159. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_activity_claude_runs.py +0 -0
  160. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_activity_collectors.py +0 -0
  161. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_activity_core.py +0 -0
  162. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_activity_live_tracking.py +0 -0
  163. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_activity_macos.py +0 -0
  164. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_activity_os_setup.py +0 -0
  165. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_activity_pipeline.py +0 -0
  166. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agent_activity.py +0 -0
  167. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agent_activity_tracking.py +0 -0
  168. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agent_chat_features.py +0 -0
  169. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agent_crud_cli.py +0 -0
  170. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agent_prompt_async.py +0 -0
  171. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agent_prompt_models.py +0 -0
  172. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agent_run_ticket.py +0 -0
  173. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agent_schedules.py +0 -0
  174. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agent_sharing.py +0 -0
  175. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agent_workspace_settings.py +0 -0
  176. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_agents_connectors_builder.py +0 -0
  177. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_allowed_tools_config.py +0 -0
  178. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_approval_park.py +0 -0
  179. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_auto_agent_dispatch.py +0 -0
  180. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_auto_ticket_queue.py +0 -0
  181. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_browser_profile.py +0 -0
  182. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_cheap_tier.py +0 -0
  183. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_claude_usage_gate.py +0 -0
  184. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_client_jira_projects.py +0 -0
  185. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_config_token_diagnostics.py +0 -0
  186. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_configure_token_owner.py +0 -0
  187. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_console_encoding.py +0 -0
  188. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_crash_recovery.py +0 -0
  189. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_create_safe_retry.py +0 -0
  190. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_deferred_tickets.py +0 -0
  191. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_departments.py +0 -0
  192. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_done_transition.py +0 -0
  193. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_epic_decompose_run.py +0 -0
  194. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_epic_inherited_agent_id.py +0 -0
  195. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_epic_session_isolation.py +0 -0
  196. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_estimator.py +0 -0
  197. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_fallback_cli.py +0 -0
  198. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_fallback_cli_upgrade.py +0 -0
  199. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_firewall_open_on_update.py +0 -0
  200. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_formats.py +0 -0
  201. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_git_action.py +0 -0
  202. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_github_fetch.py +0 -0
  203. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_github_ingest.py +0 -0
  204. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_graph_recon.py +0 -0
  205. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_graphify_build.py +0 -0
  206. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_graphify_hook_install.py +0 -0
  207. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_health.py +0 -0
  208. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_help_actions.py +0 -0
  209. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_help_attachments.py +0 -0
  210. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_help_questions.py +0 -0
  211. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_helpdesk.py +0 -0
  212. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_infographic_data.py +0 -0
  213. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_intake_job_outcome.py +0 -0
  214. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_intake_plan_review.py +0 -0
  215. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_journal.py +0 -0
  216. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_limit_fallback_handoff.py +0 -0
  217. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_local_exec_provider.py +0 -0
  218. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_mcp_route_contract.py +0 -0
  219. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_memory_guard.py +0 -0
  220. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_model_policy.py +0 -0
  221. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_panel_help.py +0 -0
  222. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_panel_ticket_schedules.py +0 -0
  223. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_parallel_tickets.py +0 -0
  224. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_phase_timer.py +0 -0
  225. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_plan_new_command.py +0 -0
  226. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_plan_run_activity.py +0 -0
  227. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_pr_identity.py +0 -0
  228. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_prd_artifact.py +0 -0
  229. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_project_path_resolution.py +0 -0
  230. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_proof_readers.py +0 -0
  231. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_providers.py +0 -0
  232. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_recon_session_isolation.py +0 -0
  233. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_resume.py +0 -0
  234. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_router.py +0 -0
  235. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_run_auto_agent_only.py +0 -0
  236. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_run_outputs.py +0 -0
  237. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_run_verdict_is_honest.py +0 -0
  238. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_schedule_api.py +0 -0
  239. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_schedule_outputs_and_plans.py +0 -0
  240. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_schedule_tickets.py +0 -0
  241. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_scheduler.py +0 -0
  242. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_scheduler_transient_retry.py +0 -0
  243. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_self_report.py +0 -0
  244. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_service_token_warning.py +0 -0
  245. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_stale_panel_restart.py +0 -0
  246. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_swagger_command.py +0 -0
  247. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_task_file_and_plan_run.py +0 -0
  248. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_task_file_format_doc.py +0 -0
  249. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_ticket_board.py +0 -0
  250. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_ticket_cli.py +0 -0
  251. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_ticket_fields.py +0 -0
  252. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_ticket_run_resolve_by_id.py +0 -0
  253. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_ticket_schedule_and_agent.py +0 -0
  254. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_ticket_speed_fixes.py +0 -0
  255. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_tier_flag_parity.py +0 -0
  256. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_ui_gate.py +0 -0
  257. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_ui_lifespan.py +0 -0
  258. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_update_timer.py +0 -0
  259. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_updater_extras.py +0 -0
  260. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_updater_ignores_cwd.py +0 -0
  261. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_updater_pep668.py +0 -0
  262. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_updater_reports_the_truth.py +0 -0
  263. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_url_access.py +0 -0
  264. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_vps_health.py +0 -0
  265. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_vps_only_reporting.py +0 -0
  266. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_windows_loop_identity.py +0 -0
  267. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_windows_panel_task.py +0 -0
  268. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_windows_stop_and_pidfile.py +0 -0
  269. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/tests/test_windows_task_paths.py +0 -0
  270. {devops_bot_sdk-1.7.5 → devops_bot_sdk-1.7.8}/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.7.5
3
+ Version: 1.7.8
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.7.5
3
+ Version: 1.7.8
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.7.5
3
+ Version: 1.7.8
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.7.5" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.7.8" # 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
@@ -2758,7 +2758,7 @@ class BackendClient:
2758
2758
  url = self._url("/api/v1/agents/shared")
2759
2759
  async with httpx.AsyncClient(timeout=15.0) as client:
2760
2760
  resp = await client.get(url, headers=self._headers)
2761
- if resp.status_code == 404:
2761
+ if resp.status_code in (404, 405): # 405: an older backend's /agents/{name} catch-all
2762
2762
  raise BackendFeatureMissing("Agent sharing", url)
2763
2763
  _check_status(resp.status_code, self._base_url)
2764
2764
  return resp.json().get("agents", [])
@@ -104,6 +104,11 @@ stuck In Progress after a crash → `restore-stranded-tickets` (⚠ moves ticket
104
104
  pending invite per recipient on the AI backend (devops-bot-ai, Mongo `agent_shares`); it reaches them only
105
105
  after they `agentos agent accept ID` (or Accept on the Custom Agents page). Accepted
106
106
  shares are runnable (`sdk/agents/chat.resolve_agent` reads `GET /api/v1/agents/shared`).
107
+ Accepted shares appear on Custom Agents next to your own (`GET /api/ui/agents` and
108
+ `/api/ui/agents/custom` add them with `source="shared"`, `shared_by`, `via`,
109
+ `share_id`): Run dropdown, installed cards and a "Shared with you" platform group,
110
+ with Run / Remove only. Their inputs come from the backend row (`/api/ui/agents/vars`
111
+ falls back to `resolve_agent`).
107
112
  The panel's Share dialog (Custom Agents → Share) has Person / Team / Department tabs;
108
113
  each lists from `GET /api/v1/agents/share-targets?kind=…` (users, teams, departments
109
114
  in the platform's SQL — names, never ids) and sends the picks as `user:<id>` /
@@ -200,8 +205,10 @@ the fix is not in their code, `agentos-auto update` installs the latest.
200
205
  `agentos-auto logs [-f]` and a foreground run colour lines in a terminal
201
206
  (`sdk/log_colors.py`): every status change prints `● KEY → Done` (green) /
202
207
  `→ To Do` (cyan) / `→ In Progress` (yellow) / `→ Backlog` (magenta); failures red,
203
- warnings and parked tickets yellow. The log file stays plain. No colour → the output
204
- isn't a terminal or `NO_COLOR` / `AGENTOS_COLOR=never` is set; `AGENTOS_COLOR=always`
208
+ warnings, parked and partial yellow, `— ok` agent runs green. Same colours in Windows
209
+ PowerShell/cmd (ANSI mode is enabled on the console) and on the panel's Runs live log
210
+ and Schedules run history. The log file stays plain. No colour → the output isn't a
211
+ terminal, an old Windows console refused ANSI mode, or `NO_COLOR` / `AGENTOS_COLOR=never` is set; `AGENTOS_COLOR=always`
205
212
  forces it. `agentos tickets` (= `agentos ticket list`, `sdk/ticket_board.py`) prints
206
213
  the counts per status and the tickets in each; `agentos-auto status` ends with a
207
214
  one-line `board:` count. The control panel shows **Backlog** next to To Do / In Progress / Done: a
@@ -9,8 +9,11 @@ the panel, Help and grep, so colour codes never go into it. Instead
9
9
  ● KEY → In Progress yellow ▶ run headers bold
10
10
  ● KEY → Backlog magenta ticket keys (ABC-123) bold, [timestamps] dim
11
11
 
12
+ ✓ / completed / "— ok" green ↳ transcript pointers, "… idle" dim
13
+
12
14
  Colour is on only for a terminal, and honours ``NO_COLOR`` (any value → off) and
13
- ``AGENTOS_COLOR=always|never``.
15
+ ``AGENTOS_COLOR=always|never``. On Windows (PowerShell, cmd, Windows Terminal) the
16
+ console's ANSI mode is switched on first; a console that refuses gets plain text.
14
17
  """
15
18
  from __future__ import annotations
16
19
 
@@ -31,29 +34,64 @@ _ARROW = re.compile(r"(→\s*)(Done|To Do|In Progress|Backlog|backlog)\b")
31
34
  _MOVED = re.compile(r"\b((?:moved (?:back )?to|returned to|status →)\s+)(Done|To Do|In Progress|Backlog|backlog)\b",
32
35
  re.IGNORECASE)
33
36
  _BAD = re.compile(r"(✗|\bTraceback\b|\berror\b|\bfailed\b|\bnon-compliant\b|\bFAILED\b)", re.IGNORECASE)
34
- _WARN = re.compile(r"(⚠|\bwarning\b|\bparked\b|⏸)", re.IGNORECASE)
35
- _GOOD = re.compile(r"(✓|\bcompleted\b)")
37
+ _WARN = re.compile(r"(⚠|\bwarning\b|\bparked\b|⏸|◐|\bpartial\b)", re.IGNORECASE)
38
+ _GOOD = re.compile(r"(✓|\bcompleted\b|— ok\b)")
39
+ _POINTER = re.compile(r"^(\s*)(↳ \w+:)")
40
+ _IDLE = re.compile(r"^\s*…\s*(?:idle|waiting)\b")
36
41
  _HEADER = re.compile(r"^\s*(?:\[[^\]]*\]\s*)?▶")
37
42
 
38
43
 
44
+ _win_vt: bool | None = None
45
+
46
+
47
+ def _windows_vt() -> bool:
48
+ """Turn on ANSI escape handling for this Windows console (once). conhost —
49
+ what PowerShell and cmd run in — prints raw ``←[32m`` without it."""
50
+ global _win_vt
51
+ if _win_vt is None:
52
+ _win_vt = False
53
+ try:
54
+ import ctypes
55
+ from ctypes import wintypes
56
+ k32 = ctypes.WinDLL("kernel32", use_last_error=True)
57
+ k32.GetStdHandle.restype = wintypes.HANDLE
58
+ ok = False
59
+ for std in (-11, -12): # STD_OUTPUT_HANDLE, STD_ERROR_HANDLE
60
+ h, mode = k32.GetStdHandle(std), wintypes.DWORD()
61
+ if h and k32.GetConsoleMode(h, ctypes.byref(mode)):
62
+ # ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING
63
+ ok = bool(k32.SetConsoleMode(h, mode.value | 0x0001 | 0x0004)) or ok
64
+ _win_vt = ok
65
+ except Exception: # noqa: BLE001
66
+ _win_vt = False
67
+ return _win_vt
68
+
69
+
39
70
  def enabled(stream=None) -> bool:
40
- """Colour this stream? Env first, then whether it is a terminal."""
71
+ """Colour this stream? Env first, then whether it is a terminal (and, on
72
+ Windows, whether its console accepts ANSI colour)."""
41
73
  if os.environ.get("NO_COLOR"):
42
74
  return False
43
75
  pref = (os.environ.get("AGENTOS_COLOR") or "").strip().lower()
44
76
  if pref in ("always", "1", "on", "yes"):
77
+ if sys.platform == "win32":
78
+ _windows_vt()
45
79
  return True
46
80
  if pref in ("never", "0", "off", "no"):
47
81
  return False
48
82
  stream = stream or sys.stdout
49
83
  try:
50
- return bool(stream.isatty())
84
+ tty = bool(stream.isatty())
51
85
  except Exception: # noqa: BLE001
52
86
  return False
87
+ if tty and sys.platform == "win32":
88
+ return _windows_vt()
89
+ return tty
53
90
 
54
91
 
55
92
  def _paint(color: str, text: str) -> str:
56
- return f"{color}{text}{_RESET}"
93
+ # An inner span's reset would end the outer colour too — re-open it after each.
94
+ return f"{color}{text.replace(_RESET, _RESET + color)}{_RESET}"
57
95
 
58
96
 
59
97
  def colorize(line: str) -> str:
@@ -75,6 +113,7 @@ def colorize(line: str) -> str:
75
113
  body = _MOVED.sub(_status, body)
76
114
  body = _KEY.sub(lambda m: _paint(BOLD, m.group(0)), body)
77
115
  body = _STAMP.sub(lambda m: m.group(1) + _paint(DIM, m.group(2)), body)
116
+ body = _POINTER.sub(lambda m: m.group(1) + _paint(DIM, m.group(2)), body)
78
117
 
79
118
  if _HEADER.match(line):
80
119
  body = _paint(BOLD + BLUE, body)
@@ -82,6 +121,8 @@ def colorize(line: str) -> str:
82
121
  body = _paint(YELLOW, body)
83
122
  elif _GOOD.search(line):
84
123
  body = _paint(GREEN, body)
124
+ elif _IDLE.match(line):
125
+ body = _paint(DIM, body)
85
126
  return body + nl
86
127
 
87
128
 
@@ -16,7 +16,7 @@ _ASCII_FALLBACK = str.maketrans({
16
16
  })
17
17
 
18
18
 
19
- def safe_print(message: str = "") -> None:
19
+ def safe_print(message: str = "", **kw) -> None:
20
20
  """``print`` that cannot take a command down on a legacy Windows codepage.
21
21
 
22
22
  :func:`ensure_utf8_stdio` is the real fix and every entry point installs it.
@@ -31,14 +31,15 @@ def safe_print(message: str = "") -> None:
31
31
  ``run_auto._send``, the glyph raises inside the ERROR handler, so the real
32
32
  error is replaced by a UnicodeEncodeError traceback and is never seen at all.
33
33
  """
34
+ # ``end``/``flush``/``file`` pass through to print (run_auto's status lines flush).
34
35
  try:
35
- print(message)
36
+ print(message, **kw)
36
37
  except UnicodeEncodeError:
37
38
  try:
38
- print(message.translate(_ASCII_FALLBACK))
39
+ print(message.translate(_ASCII_FALLBACK), **kw)
39
40
  except UnicodeEncodeError:
40
41
  # A glyph outside the table (engine output echoed into the line).
41
- print(message.encode("ascii", "replace").decode("ascii"))
42
+ print(message.encode("ascii", "replace").decode("ascii"), **kw)
42
43
 
43
44
 
44
45
  def ensure_utf8_stdio() -> None:
@@ -1817,13 +1817,13 @@ def _run_loop(workspace_root: str | None, only_keys: list[str] | None = None,
1817
1817
  + ", ".join(_describe_parked(p) for p in parked)) if parked else ""
1818
1818
  if continuous:
1819
1819
  _idle_s = _idle_refetch_s
1820
- print(f"… idle — watching for new To Do tickets "
1820
+ _print(f"… idle — watching for new To Do tickets "
1821
1821
  f"(re-checking every {int(_idle_s)}s{waiting}; "
1822
1822
  f"`agentos-auto stop` to end)")
1823
1823
  else:
1824
1824
  _idle_s = _PARK_RECHECK_S
1825
1825
  if waiting != last_wait: # say it once per change, not every minute
1826
- print(f"… waiting{waiting} (re-checking every {int(_idle_s)}s; "
1826
+ _print(f"… waiting{waiting} (re-checking every {int(_idle_s)}s; "
1827
1827
  f"`agentos-auto skip KEY` drops one, `agentos-auto stop` ends)")
1828
1828
  last_wait = waiting
1829
1829
  _waited = 0.0
@@ -466,6 +466,37 @@ async def product_agents(product_id: str = Query(...)) -> dict:
466
466
  return {"response": [], "product_id": product_id, "error": str(exc)}
467
467
 
468
468
 
469
+ async def _shared_agent_rows(client) -> tuple[list[dict], str]:
470
+ """Agents others shared with you that you ACCEPTED, as ``(rows, error)``.
471
+
472
+ Each row is the backend agent row plus ``source="shared"``, ``shared_by`` (a
473
+ NAME), ``share_id`` (for Remove) and ``via`` ("team X" / "department X").
474
+ A backend without sharing has none — that is not an error to show."""
475
+ from sdk.exceptions import BackendFeatureMissing
476
+
477
+ fetch = getattr(client, "list_shared_agents", None)
478
+ if fetch is None:
479
+ return [], ""
480
+ try:
481
+ rows = await fetch()
482
+ except BackendFeatureMissing:
483
+ return [], ""
484
+ except Exception as exc: # noqa: BLE001
485
+ return [], f"Couldn't load agents shared with you: {exc}"
486
+ out = []
487
+ for r in rows or []:
488
+ if not isinstance(r, dict) or not r.get("name"):
489
+ continue
490
+ by = r.get("shared_by") or {}
491
+ via = r.get("via_team") or {}
492
+ out.append({**r, "source": "shared",
493
+ "shared_by": (by.get("name") or by.get("email") or "") if isinstance(by, dict) else str(by),
494
+ "share_id": r.get("share_id") or "",
495
+ "via": (f"{'department' if via.get('kind') == 'department' else 'team'} {via['name']}"
496
+ if isinstance(via, dict) and via.get("name") else "")})
497
+ return out, ""
498
+
499
+
469
500
  @router.get("/agents", dependencies=[_auth])
470
501
  async def agents() -> dict:
471
502
  """The caller's own custom agents (``relation_type='User'``) from the backend.
@@ -480,14 +511,21 @@ async def agents() -> dict:
480
511
  from sdk.client import BackendClient
481
512
  from sdk.exceptions import BackendFeatureMissing
482
513
 
514
+ client = BackendClient.from_config()
515
+ # Accepted shares sit next to your own (source="shared"), so the Custom Agents
516
+ # page lists and runs them too; your own agent wins a name clash.
517
+ shared, shared_error = await _shared_agent_rows(client)
483
518
  try:
484
- rows = await BackendClient.from_config().list_custom_agents()
519
+ rows = await client.list_custom_agents()
485
520
  except BackendFeatureMissing:
486
- return {"agents": [], "error": ("Custom agents aren't available on this "
487
- "backend — it may be running an older version.")}
521
+ return {"agents": shared, "shared_error": shared_error,
522
+ "error": ("Custom agents aren't available on this "
523
+ "backend — it may be running an older version.")}
488
524
  except Exception as exc: # noqa: BLE001 — degrade to empty, surface the reason
489
- return {"agents": [], "error": str(exc)}
490
- return {"agents": rows, "error": ""}
525
+ return {"agents": shared, "error": str(exc), "shared_error": shared_error}
526
+ own = {r.get("name") for r in rows}
527
+ return {"agents": rows + [s for s in shared if s["name"] not in own], "error": "",
528
+ "shared_error": shared_error}
491
529
 
492
530
 
493
531
  def _flat_agent(row: dict, source: str, *, chattable: bool, department_id: str = "") -> dict:
@@ -640,6 +678,14 @@ async def agent_vars(name: str = Query(...)) -> dict:
640
678
  from sdk.agents import varstore as agent_varstore
641
679
 
642
680
  manifest = next((m for m in load_agents() if m.name == name), None)
681
+ if manifest is None:
682
+ # Platform agents (yours, department, shared with you) live on the backend,
683
+ # not in ~/.agentos/agents — read their prompt there so the inputs render.
684
+ try:
685
+ from sdk.agents.chat import resolve_agent
686
+ manifest = await resolve_agent(name)
687
+ except Exception: # noqa: BLE001 — unknown / unreachable: no inputs to show
688
+ manifest = None
643
689
  names = extract_variables(manifest.prompt) if manifest else []
644
690
  values = agent_varstore.load_vars(name)
645
691
  return {"name": name, "variables": names,
@@ -1816,11 +1862,19 @@ async def custom_agents_list() -> dict:
1816
1862
  backend, which decides what is yours."""
1817
1863
  from sdk import custom_agents
1818
1864
 
1819
- scopes, err = await custom_agents.list_agents(_ticket_client())
1865
+ client = _ticket_client()
1866
+ scopes, err = await custom_agents.list_agents(client)
1820
1867
  if scopes is None:
1821
1868
  raise HTTPException(status_code=502, detail=err)
1822
- return {scope: [custom_agents.editable_view(r) for r in rows]
1823
- for scope, rows in scopes.items()}
1869
+ out = {scope: [custom_agents.editable_view(r) for r in rows]
1870
+ for scope, rows in scopes.items()}
1871
+ # Accepted shares: read-only here (Run / Remove), never Edit or Delete.
1872
+ shared, shared_error = await _shared_agent_rows(client)
1873
+ out["shared"] = [{"id": s.get("id"), "name": s["name"], "status": s.get("status"),
1874
+ "shared_by": s["shared_by"], "via": s["via"], "share_id": s["share_id"]}
1875
+ for s in shared]
1876
+ out["shared_error"] = shared_error
1877
+ return out
1824
1878
 
1825
1879
 
1826
1880
  @router.post("/agents/custom", dependencies=[_auth])
@@ -131,6 +131,30 @@
131
131
  const STATUS_COLOR = { "To Do": "var(--blue)", "In Progress": "var(--amber)", "Done": "var(--green)", "Backlog": "var(--violet)" };
132
132
  function esc(s) { return String(s == null ? "" : s).replace(/[&<>"]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c])); }
133
133
 
134
+ // One log line as coloured HTML — the panel's twin of sdk/log_colors.colorize:
135
+ // statuses in their own colour, errors red, warnings amber, ▶ headers bold,
136
+ // ticket keys bold, [timestamps] dim. Never changes the text itself.
137
+ const LOG_STATUS = { "done": "lg-done", "to do": "lg-todo", "in progress": "lg-prog", "backlog": "lg-back" };
138
+ function colorLogLine(line) {
139
+ let h = esc(line);
140
+ if (!line.trim()) return h;
141
+ const arrow = /(→\s*)(Done|To Do|In Progress|Backlog|backlog)\b/;
142
+ if (/(✗|\bTraceback\b|\berror\b|\bfailed\b|\bnon-compliant\b)/i.test(line) && !arrow.test(line))
143
+ return `<span class="lg-bad">${h}</span>`;
144
+ const status = (_, pre, word) => `${pre}<b class="${LOG_STATUS[word.toLowerCase()] || ""}">${word}</b>`;
145
+ h = h.replace(/(→\s*)(Done|To Do|In Progress|Backlog|backlog)\b/g, status)
146
+ .replace(/\b((?:moved (?:back )?to|returned to|status →)\s+)(Done|To Do|In Progress|Backlog|backlog)\b/gi, status)
147
+ .replace(/\b[A-Z][A-Z0-9]{1,9}-\d+\b/g, m => `<b>${m}</b>`)
148
+ .replace(/^(\s*(?:•\s*)?)(\[\d{4}-\d{2}-\d{2}[ T][\d:]+Z?\])/, (_, pre, ts) => `${pre}<span class="lg-dim">${ts}</span>`)
149
+ .replace(/(↳ transcript:)/, `<span class="lg-dim">$1</span>`);
150
+ if (/^\s*(?:\[[^\]]*\]\s*)?▶/.test(line)) return `<span class="lg-head">${h}</span>`;
151
+ if (/(⚠|\bwarning\b|\bparked\b|⏸|\bpartial\b)/i.test(line) && !line.includes("→")) return `<span class="lg-warn">${h}</span>`;
152
+ if (/(✓|\bcompleted\b|— ok\b)/.test(line)) return `<span class="lg-good">${h}</span>`;
153
+ if (/^\s*…\s*idle\b/.test(line)) return `<span class="lg-dim">${h}</span>`;
154
+ return h;
155
+ }
156
+ function colorLog(text) { return String(text || "").split("\n").map(colorLogLine).join("\n"); }
157
+
134
158
  // Parse a "KEY=VALUE per line" textarea into an object. Used for ephemeral
135
159
  // run-time secrets (never persisted). Blank lines / lines without "=" ignored.
136
160
  function parseSecrets(id) {
@@ -457,8 +481,10 @@
457
481
  async function act(path, method, okMsg) {
458
482
  try { await api(path, { method }); toast(okMsg, "ok"); }
459
483
  catch (e) { toast(e.message, "err"); }
460
- loadShared();
484
+ // Accept / Remove changes which agents you have: the lists above reload too.
485
+ document.dispatchEvent(new CustomEvent("agentos:shares-changed"));
461
486
  }
487
+ document.addEventListener("agentos:shares-changed", () => loadShared());
462
488
  async function loadShared() {
463
489
  const box = $("sh-list");
464
490
  document.querySelectorAll("[data-shtab]").forEach(b => b.classList.toggle("primary", b.dataset.shtab === tab));
@@ -488,7 +514,7 @@
488
514
  return `<tr><td><div class="nm">${esc(x.agent_name)}</div>${x.message ? `<div class="sub">“${esc(x.message)}”</div>` : ""}</td>
489
515
  <td>${esc(who || "?")}${via}</td><td>${pill(x.status)}</td><td class="sub mono">${esc(when)}</td><td class="acts">${acts}</td></tr>`;
490
516
  }).join("") + `</tbody></table>`;
491
- box.querySelectorAll("[data-acc]").forEach(b => b.onclick = () => act(`/api/ui/agents/shares/${b.dataset.acc}/accept`, "POST", "Accepted — it is now in your agents"));
517
+ box.querySelectorAll("[data-acc]").forEach(b => b.onclick = () => act(`/api/ui/agents/shares/${b.dataset.acc}/accept`, "POST", "Accepted — it's now in your custom agents"));
492
518
  box.querySelectorAll("[data-dec]").forEach(b => b.onclick = () => act(`/api/ui/agents/shares/${b.dataset.dec}/decline`, "POST", "Declined"));
493
519
  box.querySelectorAll("[data-rm]").forEach(b => b.onclick = () => {
494
520
  if (!confirm(tab === "incoming" ? "Remove this shared agent from your list?" : "Revoke this share? They lose access.")) return;
@@ -1094,17 +1120,27 @@
1094
1120
  return;
1095
1121
  }
1096
1122
  caMeta = {}; rows.forEach(a => { caMeta[a.name] = a; });
1123
+ const own = rows.filter(a => a.source !== "shared"), shared = rows.filter(a => a.source === "shared");
1097
1124
  const kaSel = $("key-agent");
1098
1125
  if (kaSel) {
1099
- kaSel.innerHTML = rows.length
1100
- ? rows.map(a => `<option value="${esc(a.name)}">${esc(a.name)}</option>`).join("")
1101
- : `<option value="">(no agents yet)</option>`;
1126
+ const opt = (a) => `<option value="${esc(a.name)}">${esc(a.name)}${a.shared_by ? " — from " + esc(a.shared_by) : ""}</option>`;
1127
+ kaSel.innerHTML = !rows.length ? `<option value="">(no agents yet)</option>`
1128
+ : !shared.length ? own.map(opt).join("")
1129
+ : (own.length ? `<optgroup label="Your agents">${own.map(opt).join("")}</optgroup>` : "")
1130
+ + `<optgroup label="Shared with you">${shared.map(opt).join("")}</optgroup>`;
1102
1131
  onAgentSelect();
1103
1132
  }
1104
1133
  // Custom agents: full control for everyone (view / Edit-content / enable /
1105
- // disable / delete) — RBAC applies only to department agents.
1134
+ // disable / delete) — RBAC applies only to department agents. Agents shared
1135
+ // with you (accepted) are someone else's: Run and Remove only.
1106
1136
  $("local-agents").innerHTML = rows.length ? rows.map(a => {
1107
1137
  const enc = encodeURIComponent(a.name);
1138
+ if (a.source === "shared") {
1139
+ const from = `<span class="pill shared">shared</span> <span class="muted" style="font-size:12.5px">from <b>${esc(a.shared_by || "?")}</b>${a.via ? " · via " + esc(a.via) : ""}</span>`;
1140
+ return agentCardHTML(a, `${from}<span class="spacer" style="flex:1"></span>
1141
+ <a class="btn primary" href="/agents.html?agent=${enc}">Run</a>
1142
+ ${a.share_id ? `<button class="btn ghost danger" data-unshare="${esc(a.share_id)}" data-name="${esc(a.name)}">Remove</button>` : ""}`);
1143
+ }
1108
1144
  const actions = [
1109
1145
  `<a class="btn primary" href="/agents.html?agent=${enc}">Run</a>`,
1110
1146
  `<button class="btn" data-edit="${esc(a.name)}">Edit</button>`,
@@ -1117,6 +1153,7 @@
1117
1153
  : "<div class='empty'>No custom agents assigned to you yet. Create one under <b>Your platform agents</b> below, or in the web app.</div>";
1118
1154
 
1119
1155
  wireContentToggles($("local-agents"));
1156
+ $("local-agents").querySelectorAll("[data-unshare]").forEach(b => b.onclick = () => removeShared(b.dataset.unshare, b.dataset.name));
1120
1157
  $("local-agents").querySelectorAll("[data-enable]").forEach(b => b.onclick = () => setStatus(b.dataset.enable, "active"));
1121
1158
  $("local-agents").querySelectorAll("[data-disable]").forEach(b => b.onclick = () => setStatus(b.dataset.disable, "disabled"));
1122
1159
  $("local-agents").querySelectorAll("[data-del]").forEach(b => b.onclick = async () => {
@@ -1187,6 +1224,17 @@
1187
1224
  if ($("key-add-toggle")) $("key-add-toggle").textContent = "+ Add API key";
1188
1225
  }
1189
1226
 
1227
+ // Drop an agent someone shared with you (they can share it again later).
1228
+ async function removeShared(shareId, name) {
1229
+ if (!confirm(`Remove “${name}” from your agents? The owner can share it with you again.`)) return;
1230
+ try {
1231
+ await api(`/api/ui/agents/shares/${encodeURIComponent(shareId)}`, { method: "DELETE" });
1232
+ toast("Removed " + name, "ok");
1233
+ } catch (e) { toast("Remove failed: " + (e.message || ""), "err"); }
1234
+ document.dispatchEvent(new CustomEvent("agentos:shares-changed"));
1235
+ }
1236
+ document.addEventListener("agentos:shares-changed", () => { loadLocalAgents(); cfLoad(); });
1237
+
1190
1238
  function onAgentSelect() {
1191
1239
  const m = caMeta[$("key-agent") && $("key-agent").value];
1192
1240
  if ($("ca-about")) $("ca-about").textContent = m ? (m.description || "No description provided.") : "Pick an agent.";
@@ -1481,10 +1529,19 @@
1481
1529
  <button class="btn danger cf-del" data-id="${esc(a.id)}" data-name="${esc(a.name)}">Delete</button>`
1482
1530
  : `<span class="muted" style="font-size:12px">read-only</span>`}
1483
1531
  </div>`;
1484
- const personal = d.personal || [], dept = d.department || [];
1485
- box.innerHTML = (personal.length || dept.length)
1532
+ const personal = d.personal || [], dept = d.department || [], shared = d.shared || [];
1533
+ const sharedRow = (a) => `<div class="cf-item">
1534
+ <span class="n">${esc(a.name)}</span>
1535
+ <span class="pill shared">shared</span>
1536
+ <span class="muted" style="font-size:12.5px">from <b>${esc(a.shared_by || "?")}</b>${a.via ? " · via " + esc(a.via) : ""}</span>
1537
+ <a class="btn" href="/agents.html?agent=${encodeURIComponent(a.name)}">Run</a>
1538
+ ${a.share_id ? `<button class="btn ghost danger cf-unshare" data-id="${esc(a.share_id)}" data-name="${esc(a.name)}">Remove</button>` : ""}
1539
+ </div>`;
1540
+ box.innerHTML = (personal.length || dept.length || shared.length)
1486
1541
  ? personal.map(a => row(a, true)).join("") + dept.map(a => row(a, false)).join("")
1542
+ + (shared.length ? `<div class="cf-group">Shared with you</div>` + shared.map(sharedRow).join("") : "")
1487
1543
  : `<div class="empty">No platform agents yet — press <b>New agent</b>.</div>`;
1544
+ box.querySelectorAll(".cf-unshare").forEach(b => b.onclick = () => removeShared(b.dataset.id, b.dataset.name));
1488
1545
  box.querySelectorAll(".cf-edit").forEach(b => b.onclick = () => cfOpen(b.dataset.id));
1489
1546
  box.querySelectorAll(".cf-del").forEach(b => b.onclick = () => cfDelete(b.dataset.id, b.dataset.name));
1490
1547
  } catch (e) {
@@ -2353,7 +2410,7 @@
2353
2410
  ev.preventDefault();
2354
2411
  loadHistory(a.getAttribute("data-file"));
2355
2412
  }));
2356
- $("sc-file-body").textContent = d.content || "";
2413
+ $("sc-file-body").innerHTML = colorLog(d.content);
2357
2414
  } catch (e) { toast("Could not load run history", "err"); }
2358
2415
  }
2359
2416
 
@@ -2869,7 +2926,7 @@
2869
2926
  const es = new EventSource("/api/ui/logs/stream?token=" + encodeURIComponent(TOKEN));
2870
2927
  es.onmessage = (ev) => {
2871
2928
  const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 40;
2872
- box.textContent += (box.textContent ? "\n" : "") + ev.data;
2929
+ box.insertAdjacentHTML("beforeend", (box.childNodes.length ? "\n" : "") + colorLogLine(ev.data));
2873
2930
  if (atBottom) box.scrollTop = box.scrollHeight;
2874
2931
  };
2875
2932
  es.onerror = () => { es.close(); setTimeout(connectLog, 3000); };
@@ -139,6 +139,17 @@ tr:last-child td { border-bottom: none; }
139
139
  white-space: pre-wrap; word-break: break-word;
140
140
  }
141
141
  .log:empty::before { content: "Waiting for activity…"; color: #64748b; }
142
+ /* Log colours — the panel's twin of sdk/log_colors.py (the .log box is always dark). */
143
+ .log b { color: #f1f5f9; }
144
+ .log .lg-done { color: #4ade80; }
145
+ .log .lg-todo { color: #22d3ee; }
146
+ .log .lg-prog { color: #facc15; }
147
+ .log .lg-back { color: #c084fc; }
148
+ .log .lg-bad { color: #f87171; }
149
+ .log .lg-warn { color: #fbbf24; }
150
+ .log .lg-good { color: #86efac; }
151
+ .log .lg-head { color: #93c5fd; font-weight: 700; }
152
+ .log .lg-dim { color: #64748b; }
142
153
 
143
154
  /* ── Toast ── */
144
155
  #toasts { position: fixed; right: 18px; bottom: 18px; display: flex; flex-direction: column; gap: 10px; z-index: 100; }
@@ -857,3 +868,7 @@ select:focus, input[type=number]:focus, input[type=time]:focus, input[type=datet
857
868
  .hp-ask-row { display: grid; gap: 6px; font-size: 14px; }
858
869
  .hp-ask-row input { width: 100%; }
859
870
  .hp-asks-send { justify-self: start; }
871
+
872
+ /* Agents shared with you, on the Custom Agents page */
873
+ .pill.shared { background: color-mix(in srgb, var(--violet, #7c3aed) 14%, transparent); color: var(--violet, #7c3aed); }
874
+ .cf-group { font-size: 12px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); padding: 14px 0 4px; }
@@ -168,3 +168,91 @@ class TestUiAgentsAll:
168
168
  with patch("sdk.client.BackendClient.from_config", return_value=client):
169
169
  out = await api.agents_all(department_id="d-1")
170
170
  assert [(a["name"], a["source"]) for a in out["agents"]] == [("Shared", "custom")]
171
+
172
+
173
+ def _shared_row(name, *, by="Javaid Iqbal", via=None, share_id="sh-1"):
174
+ return {**_row(name, variables=("repo",)), "relation_id": "someone-else", "source": "shared",
175
+ "share_id": share_id, "shared_by": {"id": "u-j", "name": by, "email": "j@x"},
176
+ "via_team": via}
177
+
178
+
179
+ class TestSharedAgentsOnCustomAgentsPage:
180
+ """Accepted shares appear on Custom Agents next to your own — by NAME, read-only."""
181
+
182
+ @pytest.mark.asyncio
183
+ async def test_installed_list_includes_accepted_shares(self):
184
+ from sdk.ui import api
185
+
186
+ client = SimpleNamespace(
187
+ list_custom_agents=AsyncMock(return_value=[_row("mine"), _row("clash")]),
188
+ list_shared_agents=AsyncMock(return_value=[
189
+ _shared_row("release-notes", via={"id": "d1", "name": "Machine Learning", "kind": "department"}),
190
+ _shared_row("clash")]),
191
+ )
192
+ with patch("sdk.client.BackendClient.from_config", return_value=client):
193
+ out = await api.agents()
194
+ names = [(a["name"], a.get("source")) for a in out["agents"]]
195
+ assert names == [("mine", None), ("clash", None), ("release-notes", "shared")]
196
+ shared = out["agents"][-1]
197
+ assert shared["shared_by"] == "Javaid Iqbal" and shared["share_id"] == "sh-1"
198
+ assert shared["via"] == "department Machine Learning"
199
+ assert out["shared_error"] == ""
200
+
201
+ @pytest.mark.asyncio
202
+ async def test_backend_without_sharing_is_not_an_error(self):
203
+ from sdk.exceptions import BackendFeatureMissing
204
+ from sdk.ui import api
205
+
206
+ client = SimpleNamespace(
207
+ list_custom_agents=AsyncMock(return_value=[_row("mine")]),
208
+ list_shared_agents=AsyncMock(side_effect=BackendFeatureMissing("Agent sharing", "u")),
209
+ )
210
+ with patch("sdk.client.BackendClient.from_config", return_value=client):
211
+ out = await api.agents()
212
+ assert [a["name"] for a in out["agents"]] == ["mine"] and out["shared_error"] == ""
213
+
214
+ @pytest.mark.asyncio
215
+ async def test_platform_list_has_a_shared_group(self):
216
+ from sdk.ui import api
217
+
218
+ client = SimpleNamespace(
219
+ list_gateway_agents_by_scope=AsyncMock(return_value={"personal": [], "department": []}),
220
+ list_shared_agents=AsyncMock(return_value=[_shared_row("release-notes", via={"id": "t", "name": "AI Team"})]),
221
+ )
222
+ with patch("sdk.client.BackendClient.from_config", return_value=client):
223
+ out = await api.custom_agents_list()
224
+ assert out["shared"] == [{"id": "id-release-notes", "name": "release-notes", "status": "active",
225
+ "shared_by": "Javaid Iqbal", "via": "team AI Team", "share_id": "sh-1"}]
226
+
227
+ @pytest.mark.asyncio
228
+ async def test_inputs_of_a_platform_or_shared_agent_render(self):
229
+ from sdk.ui import api
230
+
231
+ client = SimpleNamespace(
232
+ list_custom_agents=AsyncMock(return_value=[]),
233
+ list_department_agents=AsyncMock(return_value=[]),
234
+ list_shared_agents=AsyncMock(return_value=[
235
+ {**_shared_row("release-notes"),
236
+ "prompt": {"system_prompt": "Write notes for {{repo}} since {{tag}}.",
237
+ "variables": [{"name": "repo"}, {"name": "tag"}]}}]),
238
+ )
239
+ with patch("sdk.client.BackendClient.from_config", return_value=client), \
240
+ patch("sdk.agents.manifest.load_agents", return_value=[]), \
241
+ patch("sdk.agents.chat._find_agent", return_value=None):
242
+ out = await api.agent_vars(name="release-notes")
243
+ assert out["variables"] == ["repo", "tag"]
244
+
245
+ @pytest.mark.asyncio
246
+ async def test_405_from_an_old_backend_means_not_deployed(self):
247
+ from sdk.client import BackendClient
248
+ from sdk.exceptions import BackendFeatureMissing
249
+
250
+ client = BackendClient.__new__(BackendClient)
251
+ client._base_url, client._headers = "https://b", {}
252
+ client._url = lambda p: "https://b" + p
253
+ resp = SimpleNamespace(status_code=405, json=lambda: {})
254
+ http = AsyncMock()
255
+ http.__aenter__.return_value.get = AsyncMock(return_value=resp)
256
+ with patch("sdk.client.httpx.AsyncClient", return_value=http):
257
+ with pytest.raises(BackendFeatureMissing):
258
+ await client.list_shared_agents()