devops-bot-sdk 1.7.45__tar.gz → 1.7.46__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 (305) hide show
  1. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
  4. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/cli.py +18 -7
  6. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/config.py +59 -31
  7. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/faqs.py +11 -16
  8. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/firewall.py +103 -47
  9. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_data/areas.json +9 -0
  10. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_data/commands.json +11 -1
  11. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_data/help.json +5 -1
  12. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_data/pages.json +1 -1
  13. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_data/releases.json +10 -0
  14. devops_bot_sdk-1.7.46/sdk/help_data/snapshot.json +17 -0
  15. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/helpdesk_kb.md +45 -33
  16. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/swagger.py +0 -6
  17. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/api.py +60 -25
  18. devops_bot_sdk-1.7.46/sdk/ui/ip_allowlist.py +199 -0
  19. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/models.py +8 -6
  20. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/server.py +11 -8
  21. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/app.js +62 -0
  22. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/index.html +22 -0
  23. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/styles.css +9 -0
  24. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/updater.py +1 -1
  25. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_firewall_open_on_update.py +28 -12
  26. devops_bot_sdk-1.7.46/tests/test_one_allowed_ip.py +153 -0
  27. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_swagger_command.py +1 -8
  28. devops_bot_sdk-1.7.46/tests/test_ui_bruteforce_guard.py +94 -0
  29. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ui_gate.py +57 -73
  30. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ui_lifespan.py +5 -5
  31. devops_bot_sdk-1.7.45/sdk/help_data/snapshot.json +0 -17
  32. devops_bot_sdk-1.7.45/sdk/ui/ip_allowlist.py +0 -389
  33. devops_bot_sdk-1.7.45/tests/test_ui_bruteforce_guard.py +0 -170
  34. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/README.md +0 -0
  35. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  36. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  37. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/requires.txt +0 -0
  38. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  39. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/pyproject.toml +0 -0
  40. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/__init__.py +0 -0
  41. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/classifier.py +0 -0
  42. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/__init__.py +0 -0
  43. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/common.py +0 -0
  44. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/linux_auditd.py +0 -0
  45. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/macos_eslogger.py +0 -0
  46. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/collectors/windows_eventlog.py +0 -0
  47. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/models.py +0 -0
  48. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/os_setup.py +0 -0
  49. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/recorder.py +0 -0
  50. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/redact.py +0 -0
  51. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/settings.py +0 -0
  52. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/shipper.py +0 -0
  53. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/activity/spool.py +0 -0
  54. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agent_sharing.py +0 -0
  55. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agentd.py +0 -0
  56. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/__init__.py +0 -0
  57. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/activity.py +0 -0
  58. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/builder.py +0 -0
  59. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/chat.py +0 -0
  60. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/connectors.py +0 -0
  61. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/__init__.py +0 -0
  62. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/claude_code.py +0 -0
  63. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/http.py +0 -0
  64. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/mcp.py +0 -0
  65. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/n8n.py +0 -0
  66. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/engines/shell.py +0 -0
  67. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/integrations.py +0 -0
  68. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/inventory.py +0 -0
  69. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/library/__init__.py +0 -0
  70. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/manifest.py +0 -0
  71. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/prompt_studio.py +0 -0
  72. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/prompt_template.py +0 -0
  73. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/prompt_vars.py +0 -0
  74. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/run_log.py +0 -0
  75. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/schedules.py +0 -0
  76. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/secrets.py +0 -0
  77. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/ticket_run.py +0 -0
  78. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/usage.py +0 -0
  79. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/agents/varstore.py +0 -0
  80. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/browser_profile.py +0 -0
  81. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/claude_usage.py +0 -0
  82. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/client.py +0 -0
  83. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/collectors/__init__.py +0 -0
  84. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/collectors/files.py +0 -0
  85. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/collectors/process.py +0 -0
  86. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/collectors/screenshot.py +0 -0
  87. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/crucial.py +0 -0
  88. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/custom_agents.py +0 -0
  89. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/deferred_tickets.py +0 -0
  90. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/departments.py +0 -0
  91. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/__init__.py +0 -0
  92. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/assets.py +0 -0
  93. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/bootstrap.py +0 -0
  94. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/browser.py +0 -0
  95. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/compare.py +0 -0
  96. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/design_verify/loop.py +0 -0
  97. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/estimator.py +0 -0
  98. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/exceptions.py +0 -0
  99. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/fallback_handoff.py +0 -0
  100. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/formats.py +0 -0
  101. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/git_ops.py +0 -0
  102. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/github_fetch.py +0 -0
  103. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/github_ingest.py +0 -0
  104. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/graph_recon.py +0 -0
  105. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/graphify.py +0 -0
  106. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/help_actions.py +0 -0
  107. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/helpdesk.py +0 -0
  108. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/hooks/__init__.py +0 -0
  109. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/hooks/crucial_guard.py +0 -0
  110. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/instructions_template.py +0 -0
  111. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/intake.py +0 -0
  112. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ipc/__init__.py +0 -0
  113. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ipc/electron_bridge.py +0 -0
  114. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ipc/handlers.py +0 -0
  115. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/journal.py +0 -0
  116. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/local_exec.py +0 -0
  117. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/log_colors.py +0 -0
  118. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/memory_guard.py +0 -0
  119. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/model_policy.py +0 -0
  120. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/models/__init__.py +0 -0
  121. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/models/envelope.py +0 -0
  122. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/models/requests.py +0 -0
  123. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/models/responses.py +0 -0
  124. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/models/snapshots.py +0 -0
  125. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/phase_timer.py +0 -0
  126. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/plan_run.py +0 -0
  127. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/platform_compat.py +0 -0
  128. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/prd_progress.py +0 -0
  129. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/proof_readers.py +0 -0
  130. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/providers.py +0 -0
  131. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/py.typed +0 -0
  132. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/resume.py +0 -0
  133. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/router.py +0 -0
  134. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/run_auto.py +0 -0
  135. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/schedule_tickets.py +0 -0
  136. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/schedule_timeline.py +0 -0
  137. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/scheduler.py +0 -0
  138. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/scope_plan.py +0 -0
  139. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/self_report.py +0 -0
  140. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/self_report_local.py +0 -0
  141. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/sse.py +0 -0
  142. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/task_file.py +0 -0
  143. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/task_types.py +0 -0
  144. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/test.py +0 -0
  145. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/test_env.py +0 -0
  146. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/test_pipeline.py +0 -0
  147. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ticket_board.py +0 -0
  148. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ticket_bulk.py +0 -0
  149. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ticket_fields.py +0 -0
  150. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ticket_plan.py +0 -0
  151. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ticket_pools.py +0 -0
  152. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/tickets.py +0 -0
  153. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/tiers/__init__.py +0 -0
  154. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/tiers/mini_agent.py +0 -0
  155. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/__init__.py +0 -0
  156. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/dashboard.py +0 -0
  157. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/health.py +0 -0
  158. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/jobs.py +0 -0
  159. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/runner.py +0 -0
  160. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/security.py +0 -0
  161. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/service.py +0 -0
  162. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/session.py +0 -0
  163. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/setup_manager.py +0 -0
  164. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/agents.html +0 -0
  165. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/chat.html +0 -0
  166. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/custom-agents.html +0 -0
  167. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/default-agents.html +0 -0
  168. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/faqs.html +0 -0
  169. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/help.html +0 -0
  170. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/report.html +0 -0
  171. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/runs.html +0 -0
  172. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/schedules.html +0 -0
  173. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/setup.html +0 -0
  174. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/static/tickets.html +0 -0
  175. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/ui/swagger.py +0 -0
  176. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/url_access.py +0 -0
  177. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/vps_health.py +0 -0
  178. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/vps_identity.py +0 -0
  179. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/sdk/write_hook.py +0 -0
  180. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/setup.cfg +0 -0
  181. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_claude_runs.py +0 -0
  182. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_collectors.py +0 -0
  183. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_core.py +0 -0
  184. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_live_tracking.py +0 -0
  185. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_macos.py +0 -0
  186. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_os_setup.py +0 -0
  187. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_activity_pipeline.py +0 -0
  188. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_activity.py +0 -0
  189. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_activity_tracking.py +0 -0
  190. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_chat_features.py +0 -0
  191. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_crud_cli.py +0 -0
  192. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_listing.py +0 -0
  193. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_prompt_async.py +0 -0
  194. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_prompt_models.py +0 -0
  195. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_run_ticket.py +0 -0
  196. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_schedules.py +0 -0
  197. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_sharing.py +0 -0
  198. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agent_workspace_settings.py +0 -0
  199. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_agents_connectors_builder.py +0 -0
  200. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_allowed_tools_config.py +0 -0
  201. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_approval_park.py +0 -0
  202. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_auto_agent_dispatch.py +0 -0
  203. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_auto_ticket_queue.py +0 -0
  204. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_browser_profile.py +0 -0
  205. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_cheap_tier.py +0 -0
  206. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_claude_usage_gate.py +0 -0
  207. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_client_jira_projects.py +0 -0
  208. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_config_token_diagnostics.py +0 -0
  209. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_configure_token_owner.py +0 -0
  210. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_console_encoding.py +0 -0
  211. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_crash_recovery.py +0 -0
  212. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_create_safe_retry.py +0 -0
  213. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_deferred_tickets.py +0 -0
  214. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_departments.py +0 -0
  215. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_done_transition.py +0 -0
  216. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_epic_decompose_run.py +0 -0
  217. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_epic_inherited_agent_id.py +0 -0
  218. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_epic_session_isolation.py +0 -0
  219. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_estimator.py +0 -0
  220. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_fallback_cli.py +0 -0
  221. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_fallback_cli_upgrade.py +0 -0
  222. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_fallback_rate_limit.py +0 -0
  223. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_faqs.py +0 -0
  224. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_formats.py +0 -0
  225. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_git_action.py +0 -0
  226. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_github_fetch.py +0 -0
  227. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_github_ingest.py +0 -0
  228. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_graph_off_by_default.py +0 -0
  229. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_graph_recon.py +0 -0
  230. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_graphify_build.py +0 -0
  231. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_graphify_hook_install.py +0 -0
  232. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_health.py +0 -0
  233. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_help_actions.py +0 -0
  234. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_help_attachments.py +0 -0
  235. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_help_questions.py +0 -0
  236. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_helpdesk.py +0 -0
  237. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_infographic_data.py +0 -0
  238. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_instructions_template.py +0 -0
  239. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_intake_job_outcome.py +0 -0
  240. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_intake_plan_review.py +0 -0
  241. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_journal.py +0 -0
  242. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_limit_fallback_handoff.py +0 -0
  243. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_local_exec_provider.py +0 -0
  244. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_log_colors_backlog.py +0 -0
  245. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_mcp_route_contract.py +0 -0
  246. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_memory_guard.py +0 -0
  247. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_model_policy.py +0 -0
  248. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_panel_help.py +0 -0
  249. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_panel_ticket_schedules.py +0 -0
  250. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_parallel_tickets.py +0 -0
  251. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_phase_timer.py +0 -0
  252. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_plan_ahead.py +0 -0
  253. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_plan_new_command.py +0 -0
  254. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_plan_run_activity.py +0 -0
  255. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_pool_pr_modes.py +0 -0
  256. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_pr_identity.py +0 -0
  257. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_prd_artifact.py +0 -0
  258. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_project_path_resolution.py +0 -0
  259. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_proof_readers.py +0 -0
  260. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_providers.py +0 -0
  261. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_quiet_run_log.py +0 -0
  262. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_recon_session_isolation.py +0 -0
  263. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_resume.py +0 -0
  264. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_router.py +0 -0
  265. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_run_auto_agent_only.py +0 -0
  266. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_run_outputs.py +0 -0
  267. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_run_verdict_is_honest.py +0 -0
  268. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_schedule_api.py +0 -0
  269. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_schedule_outputs_and_plans.py +0 -0
  270. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_schedule_tickets.py +0 -0
  271. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_schedule_timeline.py +0 -0
  272. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_scheduler.py +0 -0
  273. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_scheduler_transient_retry.py +0 -0
  274. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_self_report.py +0 -0
  275. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_service_token_warning.py +0 -0
  276. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_stale_panel_restart.py +0 -0
  277. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_task_file_and_plan_run.py +0 -0
  278. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_task_file_format_doc.py +0 -0
  279. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_theme_switch.py +0 -0
  280. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_board.py +0 -0
  281. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_bulk.py +0 -0
  282. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_cli.py +0 -0
  283. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_detail_view.py +0 -0
  284. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_fields.py +0 -0
  285. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_plan.py +0 -0
  286. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_pools.py +0 -0
  287. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_run_resolve_by_id.py +0 -0
  288. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_schedule_and_agent.py +0 -0
  289. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ticket_speed_fixes.py +0 -0
  290. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_tier_flag_parity.py +0 -0
  291. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_ui_no_native_dialogs.py +0 -0
  292. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_update_timer.py +0 -0
  293. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_updater_extras.py +0 -0
  294. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_updater_ignores_cwd.py +0 -0
  295. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_updater_pep668.py +0 -0
  296. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_updater_reports_the_truth.py +0 -0
  297. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_url_access.py +0 -0
  298. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_version_flag.py +0 -0
  299. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_vps_health.py +0 -0
  300. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_vps_only_reporting.py +0 -0
  301. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_windows_loop_identity.py +0 -0
  302. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_windows_panel_task.py +0 -0
  303. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_windows_stop_and_pidfile.py +0 -0
  304. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/tests/test_windows_task_paths.py +0 -0
  305. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.46}/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.45
3
+ Version: 1.7.46
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.45
3
+ Version: 1.7.46
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
@@ -235,6 +235,7 @@ tests/test_log_colors_backlog.py
235
235
  tests/test_mcp_route_contract.py
236
236
  tests/test_memory_guard.py
237
237
  tests/test_model_policy.py
238
+ tests/test_one_allowed_ip.py
238
239
  tests/test_panel_help.py
239
240
  tests/test_panel_ticket_schedules.py
240
241
  tests/test_parallel_tickets.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.7.45
3
+ Version: 1.7.46
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.45" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.7.46" # 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
@@ -104,11 +104,17 @@ Command surface:
104
104
  default; `--apply` executes them and needs root / Administrator. The host
105
105
  firewall is a separate layer the allowlist cannot reach, and the two drifting
106
106
  apart is what silently blinded every VPS when the backend's egress IP moved.
107
- `--any` opens the port to every source instead, leaving
108
- IPAllowlistMiddleware as the gate — the more robust choice when the
109
- backend's egress IP can move, since a stale per-IP kernel rule drops the
110
- SYN before the allowlist is ever consulted. This is what runs
111
- automatically after a self-update. See sdk.firewall.
107
+ `--apply` also removes the open-to-any rule and any rule for an IP no
108
+ longer allowed; this is what runs automatically after a self-update.
109
+ `--any` opens the port to every source instead (not recommended: every
110
+ scanner then reaches the panel). See sdk.firewall.
111
+
112
+ agentos allow-ip [IP | --clear]
113
+ Show, set or clear THE one outside IP allowed to reach the panel (besides
114
+ this machine and the backend). Only one at a time: a new IP replaces the
115
+ old one in the allowlist and the host firewall. Same-machine only — this
116
+ command over SSH, or the panel opened at http://127.0.0.1:8765 on the box;
117
+ the remote API refuses. The firewall half needs root. See sdk.firewall.
112
118
 
113
119
  agentos swagger [--open] [--json [PATH]] [--port N]
114
120
  Where the panel's Swagger UI is and whether it can actually be reached:
@@ -223,6 +229,10 @@ def main() -> None:
223
229
  _github_command(rest)
224
230
  return
225
231
 
232
+ if noun == "allow-ip":
233
+ from sdk import firewall
234
+ sys.exit(firewall.allow_ip_cli(rest))
235
+
226
236
  if noun == "firewall":
227
237
  from sdk import firewall
228
238
  sys.exit(firewall.cli(rest))
@@ -264,7 +274,7 @@ def main() -> None:
264
274
 
265
275
 
266
276
  def _print_usage() -> None:
267
- print("usage: agentos <configure|chat|agent|ticket|integration|default-agents|agentd|github|report|browser|fallback|help|doctor|firewall|activity-setup|swagger> ...")
277
+ print("usage: agentos <configure|chat|agent|ticket|integration|default-agents|agentd|github|report|browser|fallback|help|doctor|firewall|allow-ip|activity-setup|swagger> ...")
268
278
  print()
269
279
  print(' agentos version (or --version) # installed SDK version, Python and install path')
270
280
  print(' agentos help "<your problem>" [--file PATH]… # DevOps Bot Help: reads your logs + the SDK (+ screenshots/logs you attach)')
@@ -284,7 +294,8 @@ def _print_usage() -> None:
284
294
  print(" agentos report <submit|list|show ID> # report work Jira/GitHub never saw")
285
295
  print(" agentos browser <login URL|status|logout> # sign in once; private links become readable")
286
296
  print(" agentos fallback <install|login|status|logout> # the AI Draftsman CLI used when claude is not working")
287
- print(" agentos firewall [--apply] [--revoke IP] [--port N] # open the UI port to the allowlisted IPs")
297
+ print(" agentos allow-ip [IP | --clear] # the ONE outside IP allowed to reach the panel")
298
+ print(" agentos firewall [--apply] [--revoke IP] [--port N] # limit the UI port to the allowlisted IPs")
288
299
  print(" agentos firewall --any [--apply] # open the UI port to any source (allowlist is the gate)")
289
300
  print(" agentos swagger [--open] [--json [PATH]] # where /docs is, and can you reach it")
290
301
 
@@ -227,36 +227,64 @@ def _parse_ip_list(raw: str) -> list[str]:
227
227
 
228
228
 
229
229
  def get_allowed_ips() -> list[str]:
230
- """Client IPs permitted to reach the ``agentos-ui`` API (``/api/ui/*``).
230
+ """Client IPs permitted to reach the ``agentos-ui`` server: the built-in backend
231
+ IP(s) plus AT MOST ONE user IP.
231
232
 
232
- This is the allowlist for the *inbound* half of the two-way link: the AI
233
- backend (and only it) may call back into the SDK server. Loopback is always
234
- trusted separately — this list is for the AI-backend IP(s) you whitelist.
233
+ ONE user IP at a time is the rule — the panel is reachable from a single
234
+ outside address, so a leaked token is useless anywhere else and the host
235
+ firewall (`sdk.firewall.ensure_restricted`) admits exactly that address.
236
+ Loopback and this host's own IPs are always trusted separately.
235
237
 
236
- Sources, merged (all optional), each accepting a comma/space-separated or
237
- bracketed list of strings:
238
- - ``.env`` key ``AGENTOS_UI_ALLOWED_IPS`` (``~/.agentos/.env`` or CWD ``.env``)
239
- - process env ``AGENTOS_UI_ALLOWED_IPS``
240
- - config.toml key ``allowed_ips``
241
-
242
- Returns a de-duplicated list (empty means "loopback only"). Supplying the
243
- AI-backend IP here later is all that's needed to open the link — no code
244
- change. CIDR/prefix entries are supported by the middleware.
238
+ The user IP is chosen by :func:`user_allowed_ip`; anything else configured is
239
+ ignored and reported by :func:`ignored_allowed_ips`.
245
240
  """
246
- sources = [
247
- ",".join(_DEFAULT_ALLOWED_IPS), # built-in backend IP(s) — always allowed
248
- _dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
249
- os.environ.get("AGENTOS_UI_ALLOWED_IPS", ""),
250
- _load_raw().get("allowed_ips", ""),
251
- ]
241
+ out = list(_DEFAULT_ALLOWED_IPS)
242
+ user = user_allowed_ip()
243
+ if user and user not in out:
244
+ out.append(user)
245
+ return out
246
+
247
+
248
+ def _single_address(value: str) -> bool:
249
+ """True for one host address (a bare IP, or a /32 / /128) — not a range."""
250
+ import ipaddress
251
+ try:
252
+ return ipaddress.ip_network(value.strip(), strict=False).num_addresses == 1
253
+ except ValueError:
254
+ return False
255
+
256
+
257
+ def _configured_user_ips() -> list[str]:
258
+ """Every non-backend entry configured anywhere, oldest source first:
259
+ ``.env``, process env, then config.toml (the runtime / API-added one)."""
252
260
  out: list[str] = []
253
- for src in sources:
261
+ for src in (_dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
262
+ os.environ.get("AGENTOS_UI_ALLOWED_IPS", ""),
263
+ _load_raw().get("allowed_ips", "")):
254
264
  for ip in _parse_ip_list(src):
255
- if ip not in out:
265
+ if ip not in _DEFAULT_ALLOWED_IPS and ip not in out:
256
266
  out.append(ip)
257
267
  return out
258
268
 
259
269
 
270
+ def user_allowed_ip() -> str | None:
271
+ """The ONE user IP allowed to reach the panel, or None.
272
+
273
+ The most recent wins: the runtime entry (config.toml — set by
274
+ ``POST /api/ui/allowlist``) over ``.env``, and within a source the LAST entry,
275
+ since the documented way to add an IP is to append it. CIDR ranges are never
276
+ chosen — a range is many IPs.
277
+ """
278
+ candidates = [ip for ip in _configured_user_ips() if _single_address(ip)]
279
+ return candidates[-1] if candidates else None
280
+
281
+
282
+ def ignored_allowed_ips() -> list[str]:
283
+ """Configured user IPs/ranges that are NOT allowed, because only one may be."""
284
+ keep = user_allowed_ip()
285
+ return [ip for ip in _configured_user_ips() if ip != keep]
286
+
287
+
260
288
  # Built-in baseline of tools a headless agent run may use. Kept here (not in
261
289
  # local_exec) so it is one configurable setting rather than a literal buried in the
262
290
  # spawn code: the set WILL grow, and a new tool must be addable without editing
@@ -434,23 +462,23 @@ def journal_enabled() -> bool:
434
462
 
435
463
 
436
464
  def add_allowed_ip(ip: str) -> list[str]:
437
- """Whitelist an IP/CIDR at RUNTIME (persisted to config.toml ``allowed_ips``).
465
+ """Make ``ip`` THE user IP at runtime (persisted to config.toml ``allowed_ips``).
438
466
 
439
- Idempotent. The middleware re-reads the allowlist per request, so the IP is
440
- honoured immediately (no restart) and stays until disabled. An IP already
441
- permanently whitelisted via ``.env`` is left as-is (no runtime duplicate).
442
- Returns the merged allowlist.
467
+ Only one user IP may reach the panel at a time, so this REPLACES whichever was
468
+ there before — including one from ``.env``, which it outranks while set.
469
+ Idempotent. The middleware sees it on the next request; the caller updates the
470
+ host firewall. A backend IP is already allowed and is left alone. Returns the
471
+ effective allowlist.
443
472
  """
444
473
  ip = (ip or "").strip()
445
474
  if not ip:
446
475
  raise ValueError("ip is required")
447
- if ip in permanent_allowed_ips(): # already permanent — nothing to add
476
+ if not _single_address(ip):
477
+ raise ValueError(f"{ip} is a range — only one IP may be allowed at a time")
478
+ if ip in _DEFAULT_ALLOWED_IPS:
448
479
  return get_allowed_ips()
449
480
  raw = _load_raw() # preserves token_enc / base_url / etc.
450
- entries = _parse_ip_list(raw.get("allowed_ips", ""))
451
- if ip not in entries:
452
- entries.append(ip)
453
- raw["allowed_ips"] = ", ".join(entries)
481
+ raw["allowed_ips"] = ip
454
482
  _save_raw(raw)
455
483
  return get_allowed_ips()
456
484
 
@@ -21,22 +21,17 @@ FAQS: list[tuple[str, str, str]] = [
21
21
  ("Basics", "How do I open the control panel from another machine?",
22
22
  "**Easiest — an SSH tunnel (nothing to open):** on your machine run "
23
23
  "`ssh -L 8765:127.0.0.1:8765 you@SERVER`, then browse `http://127.0.0.1:8765`.\n"
24
- "**Directly by IP:**\n"
25
- "1. On YOUR machine, find your public IP: `curl -s https://api.ipify.org; echo` → prints e.g. `203.0.113.7`\n"
26
- "2. On the SERVER, allowlist it: `echo 'AGENTOS_UI_ALLOWED_IPS=203.0.113.7' >> ~/.agentos/.env` — if that "
27
- "line is already there, add the IP to it instead (only the first line counts): "
28
- "`sed -i 's/^AGENTOS_UI_ALLOWED_IPS=.*/&,203.0.113.7/' ~/.agentos/.env`\n"
29
- "3. Check it: `grep AGENTOS_UI_ALLOWED_IPS ~/.agentos/.env` → `AGENTOS_UI_ALLOWED_IPS=203.0.113.7` "
30
- "(or your earlier IPs, then `,203.0.113.7`)\n"
31
- "4. Open the port: `agentos firewall --apply` (needs sudo rights; if it can't, it prints the `ufw` command to run)\n"
32
- "**On a Windows server** (PowerShell): `(Invoke-RestMethod https://api.ipify.org)` for your IP; "
33
- "`Add-Content \"$env:USERPROFILE\\.agentos\\.env\" \"AGENTOS_UI_ALLOWED_IPS=203.0.113.7\"` the first time, or "
34
- "`(Get-Content $f) -replace '^(AGENTOS_UI_ALLOWED_IPS=.*)$','$1,203.0.113.7' | Set-Content $f` "
35
- "(with `$f = \"$env:USERPROFILE\\.agentos\\.env\"`) to add to an existing line; "
36
- "`agentos firewall --apply` in an Administrator PowerShell. In cmd: "
37
- "`echo AGENTOS_UI_ALLOWED_IPS=203.0.113.7>> \"%USERPROFILE%\\.agentos\\.env\"` (no space before `>>`).\n"
38
- "5. Browse `http://SERVER-IP:8765` and paste your co_ token. It works at once — no restart. "
39
- "`agentos swagger` shows whether the port is reachable and which allowlisted IPs the firewall still blocks."),
24
+ "**Directly by IP** — only ONE outside IP can open the panel at a time, and only someone on the "
25
+ "server can set it:\n"
26
+ "1. On YOUR machine, find your public IP: `curl -s https://api.ipify.org; echo` → prints e.g. `203.0.113.7` "
27
+ "(Windows PowerShell: `Invoke-RestMethod https://api.ipify.org`)\n"
28
+ "2. On the SERVER (over SSH): `agentos allow-ip 203.0.113.7` — this REPLACES the previous IP, which loses access. "
29
+ "Or open the panel on the server itself (`http://127.0.0.1:8765`) → Dashboard → **Remote access**.\n"
30
+ "3. If it says the firewall needs root: `sudo agentos firewall --apply` (Windows: `agentos firewall --apply` "
31
+ "in an Administrator PowerShell).\n"
32
+ "4. Browse `http://SERVER-IP:8765` and paste your co_ token. `agentos allow-ip` shows the allowed IP; "
33
+ "`agentos allow-ip --clear` removes it. The remote API cannot change it, so a leaked token cannot "
34
+ "open the panel to another address."),
40
35
  ("Basics", "How do I use devops-bot-sdk on another machine?",
41
36
  "Install it there too — the same checklist (Python, git, SSH key, repo access, claude or ad), then "
42
37
  "`pip install \"git+ssh://git@github.com/consultancy-outfit/DevOpsBot-SDK.git\"` and `agentos configure` "
@@ -24,8 +24,7 @@ it adds a per-IP rule for every allowlisted address and REMOVES the open-to-any
24
24
  rule. It used to run ``--any``, and that left every VPS with 8765 open to the
25
25
  internet: each brute-force/scanner request then reached Python before the
26
26
  allowlist refused it, and a flood pinned the panel at 100% CPU. Open-to-any is
27
- now only kept when the operator chose it — ``AGENTOS_UI_PUBLIC=1`` (public mode
28
- needs the port reachable) or ``AGENTOS_FIREWALL_OPEN_ANY=1``.
27
+ now only kept when the operator chose it with ``AGENTOS_FIREWALL_OPEN_ANY=1``.
29
28
 
30
29
  Applying is deliberately opt-in: editing a live firewall is exactly the operation
31
30
  that can lock an operator out of their own box, so the default is to print and let
@@ -118,8 +117,8 @@ def allow_any_rules(port: int, system: str | None = None) -> list[str]:
118
117
  That reasoning held for *access* but not for *load*: every request from the
119
118
  internet still costs a Python round-trip before the allowlist refuses it, and
120
119
  a brute-force flood took panels to 100% CPU. So this is no longer the default
121
- — :func:`ensure_restricted` is — and is kept for public mode and for an
122
- operator who opts in with ``AGENTOS_FIREWALL_OPEN_ANY=1``. A backend IP move
120
+ — :func:`ensure_restricted` is — and is kept only for an operator who opts
121
+ in with ``AGENTOS_FIREWALL_OPEN_ANY=1``. A backend IP move
123
122
  is still self-healing: the new IP goes in ``_DEFAULT_ALLOWED_IPS`` and the
124
123
  next self-update adds its rule.
125
124
  """
@@ -246,24 +245,27 @@ def _net(value: str):
246
245
  def open_any_opted_in() -> bool:
247
246
  """Has the operator chosen to keep the port open to any source?
248
247
 
249
- True for ``AGENTOS_UI_PUBLIC=1`` (public mode is unreachable otherwise) or an
250
- explicit ``AGENTOS_FIREWALL_OPEN_ANY=1``. Read from the process env or
251
- ``~/.agentos/.env``, like every other ``AGENTOS_UI_*`` toggle.
248
+ Only an explicit ``AGENTOS_FIREWALL_OPEN_ANY=1`` — read from the process env
249
+ or ``~/.agentos/.env``, like every other ``AGENTOS_UI_*`` toggle. (The removed
250
+ ``AGENTOS_UI_PUBLIC=1`` used to imply it.)
252
251
  """
253
252
  try:
254
- from sdk.ui.ip_allowlist import _TRUE, _setting, public_mode
253
+ from sdk.ui.ip_allowlist import _TRUE, _setting
255
254
  except Exception: # noqa: BLE001 — never fail open on an import hiccup
256
255
  return False
257
- return public_mode() or _setting("AGENTOS_FIREWALL_OPEN_ANY").lower() in _TRUE
256
+ return _setting("AGENTOS_FIREWALL_OPEN_ANY").lower() in _TRUE
258
257
 
259
258
 
260
259
  def ensure_restricted(port: int | None = None, *, apply: bool = True,
261
260
  ips: list[str] | None = None) -> tuple[bool, str]:
262
- """Limit ``port`` to the allowlisted IPs. Returns ``(changed, message)``. Never raises.
261
+ """Limit ``port`` to EXACTLY the allowlisted IPs. Returns ``(changed, message)``.
262
+ Never raises.
263
263
 
264
264
  Adds a per-IP rule for each allowlisted address that lacks one, THEN removes
265
- the open-to-any rule — in that order, and the removal is skipped if any add
266
- failed, so the backend never loses its route mid-change. Same contract as
265
+ the open-to-any rule and every per-IP rule for an address no longer allowed
266
+ (only one user IP may reach the panel, so replacing it must close the old
267
+ one) — in that order, and the removals are skipped if any add failed, so the
268
+ backend never loses its route mid-change. Same contract as
267
269
  :func:`ensure_open` otherwise: unreadable state is never treated as closed,
268
270
  an unprivileged run only returns the commands, and macOS never applies.
269
271
 
@@ -299,18 +301,23 @@ def ensure_restricted(port: int | None = None, *, apply: bool = True,
299
301
  f"to the allowlist yourself:\n{printable}")
300
302
 
301
303
  have = {n for n in (_net(x) for x in sources) if n is not None}
304
+ want = {n for n in (_net(ip) for ip in ips) if n is not None}
302
305
  missing = [ip for ip in ips if _net(ip) is not None and _net(ip) not in have]
306
+ stale = sorted(x for x in sources if _net(x) is not None and _net(x) not in want)
303
307
  open_any = "Anywhere" in sources
304
308
 
305
- if not missing and not open_any:
309
+ if not missing and not open_any and not stale:
306
310
  return (False, f"TCP {port} is restricted to the allowlist" + inactive_note)
307
311
 
308
- rules = allow_rules(missing, port, sysname)
309
- if open_any:
310
- rules.append(revoke_any_rule(port, sysname))
312
+ removals = ([revoke_any_rule(port, sysname)] if open_any else []) + [
313
+ revoke_rule(x, port, sysname) for x in stale]
314
+ rules = allow_rules(missing, port, sysname) + removals
311
315
  printable = " " + "\n ".join(rules)
312
- what = (f"TCP {port} is open to ANY source" if open_any
313
- else f"TCP {port} is missing a rule for: {', '.join(missing)}")
316
+ what = "; ".join(w for w in (
317
+ f"TCP {port} is open to ANY source" if open_any else "",
318
+ f"missing a rule for: {', '.join(missing)}" if missing else "",
319
+ f"still allows: {', '.join(stale)}" if stale else "",
320
+ ) if w)
314
321
 
315
322
  if not apply or sysname == "darwin":
316
323
  return (False, f"{what}. To restrict it to the allowlist:\n{printable}" + inactive_note)
@@ -323,38 +330,31 @@ def ensure_restricted(port: int | None = None, *, apply: bool = True,
323
330
  # The any-rule stays: removing it now could cut the backend off.
324
331
  return (False, f"could not add allowlist rules for TCP {port}: {'; '.join(failed)}"
325
332
  f"\nThe open-to-any rule was left in place.")
326
- if open_any:
327
- failed = _apply_rules([revoke_any_rule(port, sysname)])
333
+ if removals:
334
+ _apply_rules(removals)
328
335
  still = port_sources(port, sysname, collapse=False) or set()
329
- if failed or "Anywhere" in still:
330
- return (bool(missing), f"TCP {port} is still open to any source — a rule the SDK "
331
- f"did not create allows it. Find and delete it:\n "
332
- + ("sudo ufw status numbered" if sysname == "linux" else
333
- "netsh advfirewall firewall show rule name=all dir=in")
334
- + inactive_note)
335
- return (True, f"restricted TCP {port} to the allowlist ({len(ips)} IP(s))" + inactive_note)
336
- return (True, f"added allowlist rule(s) for TCP {port}: {', '.join(missing)}" + inactive_note)
337
-
338
-
339
- def apply_ip(ip: str, *, add: bool = True, port: int | None = None) -> tuple[bool, str]:
340
- """Add (or remove) the kernel rule for one runtime-allowlisted ``ip``. Never raises.
341
-
342
- Called by the ``/api/ui/allowlist`` routes so a newly whitelisted IP is not
343
- dropped by the host firewall until the next self-update. Applies only when
344
- the panel is privileged and the port is not deliberately open to any source;
345
- otherwise returns the command for the operator to run.
336
+ left = sorted(x for x in still
337
+ if x == "Anywhere" or (_net(x) is not None and _net(x) not in want))
338
+ if left:
339
+ return (True, f"TCP {port} still allows {', '.join(left)} — a rule the SDK did "
340
+ f"not create. Find and delete it:\n "
341
+ + ("sudo ufw status numbered" if sysname == "linux" else
342
+ "netsh advfirewall firewall show rule name=all dir=in")
343
+ + inactive_note)
344
+ return (True, f"restricted TCP {port} to the allowlist: {', '.join(ips)}" + inactive_note)
345
+
346
+
347
+ def sync_rules(port: int | None = None) -> tuple[bool, str]:
348
+ """Bring the host firewall in line with the allowlist after it changed.
349
+
350
+ Called when the one user IP is set or cleared (CLI or panel), so the new IP's
351
+ rule is added and the old one's removed straight away rather than at the next
352
+ self-update. Leaves a deliberately open port alone. Never raises.
346
353
  """
347
- port = ui_port() if port is None else port
348
- sysname = _system()
349
- rule = (allow_rules([ip], port, sysname)[0] if add else revoke_rule(ip, port, sysname))
350
354
  if open_any_opted_in():
351
- return (False, "port is open to any source by choice — no per-IP rule needed")
352
- if sysname not in ("linux", "windows") or not _is_privileged():
353
- return (False, f"run on the host to update its firewall:\n {rule}")
354
- failed = _apply_rules([rule])
355
- if failed:
356
- return (False, f"firewall rule failed: {failed[0]}")
357
- return (True, f"firewall rule {'added' if add else 'removed'} for {ip}")
355
+ return (False, "port is open to any source by choice (AGENTOS_FIREWALL_OPEN_ANY) — "
356
+ "no per-IP rules to change")
357
+ return ensure_restricted(port)
358
358
 
359
359
 
360
360
  def revoke_rule(ip: str, port: int, system: str | None = None) -> str:
@@ -607,5 +607,61 @@ def cli(argv: list[str]) -> int:
607
607
  return 0 if changed or "restricted to the allowlist" in msg else 1
608
608
 
609
609
 
610
+ def allow_ip_cli(argv: list[str]) -> int:
611
+ """``agentos allow-ip [IP | --clear]`` — set, show or clear THE one user IP.
612
+
613
+ Only one outside IP may reach the panel at a time; setting a new one replaces
614
+ the old one in the allowlist and in the host firewall. This command is one of
615
+ the two ways to change it — both need someone on the machine (this one over
616
+ SSH, or the panel opened at http://127.0.0.1:8765 on the box); the remote API
617
+ refuses. Run it as the user the panel runs as, so it edits that user's
618
+ ~/.agentos/config.toml; the firewall half needs root and prints the command
619
+ otherwise.
620
+ """
621
+ from sdk import config
622
+
623
+ def show() -> None:
624
+ user = config.user_allowed_ip()
625
+ print(f"Allowed user IP : {user or '(none — only this machine and the backend)'}")
626
+ print(f"Backend IPs : {', '.join(config._DEFAULT_ALLOWED_IPS)}")
627
+ ignored = config.ignored_allowed_ips()
628
+ if ignored:
629
+ print(f"Ignored : {', '.join(ignored)} (only one user IP is allowed at a time)")
630
+
631
+ args = [a for a in argv if a.strip()]
632
+ if not args or args[0] in ("--show", "show"):
633
+ show()
634
+ return 0
635
+
636
+ if args[0] in ("--clear", "clear", "--remove"):
637
+ user = config.user_allowed_ip()
638
+ runtime = [ip for ip in config.runtime_allowed_ips() if ip == user]
639
+ if not runtime:
640
+ if user:
641
+ print(f"{user} comes from AGENTOS_UI_ALLOWED_IPS in ~/.agentos/.env — "
642
+ "remove it there.")
643
+ return 1
644
+ print("No user IP is allowed — nothing to clear.")
645
+ return 0
646
+ config.remove_allowed_ip(user)
647
+ print(f"✓ removed {user}")
648
+ else:
649
+ ip = args[0]
650
+ before = config.user_allowed_ip()
651
+ try:
652
+ config.add_allowed_ip(ip)
653
+ except ValueError as exc:
654
+ print(f"error: {exc}")
655
+ return 2
656
+ print(f"✓ {ip} is now the one IP allowed to reach the panel")
657
+ if before and before != ip:
658
+ print(f" (replaced {before} — it no longer has access)")
659
+
660
+ changed, msg = sync_rules()
661
+ print(("↻ " if changed else "") + msg)
662
+ show()
663
+ return 0
664
+
665
+
610
666
  if __name__ == "__main__": # pragma: no cover
611
667
  sys.exit(cli(sys.argv[1:]))
@@ -80,6 +80,15 @@
80
80
  "latest_pr": 143,
81
81
  "latest_date": "2026-10-16"
82
82
  },
83
+ {
84
+ "area": "General",
85
+ "changes_30d": 1,
86
+ "features_30d": 0,
87
+ "fixes_30d": 0,
88
+ "latest": "security: remove public mode; ONE outside IP at a time, set only on the server (allow-ip + Remote access)",
89
+ "latest_pr": 189,
90
+ "latest_date": "2026-10-30"
91
+ },
83
92
  {
84
93
  "area": "Local Exec",
85
94
  "changes_30d": 1,
@@ -509,12 +509,22 @@
509
509
  "doc": "https://github.com/consultancy-outfit/DevOpsBot-SDK/blob/main/docs/COMMANDS.md",
510
510
  "id": "agentos doctor"
511
511
  },
512
+ {
513
+ "command": "agentos allow-ip",
514
+ "syntax": "agentos allow-ip [IP | --clear]",
515
+ "program": "agentos",
516
+ "area": "Other",
517
+ "description": "the ONE outside IP allowed to reach the panel (on the server only)",
518
+ "valid": true,
519
+ "doc": "https://github.com/consultancy-outfit/DevOpsBot-SDK/blob/main/docs/COMMANDS.md",
520
+ "id": "agentos allow-ip"
521
+ },
512
522
  {
513
523
  "command": "agentos firewall",
514
524
  "syntax": "agentos firewall [--apply] [--any] [--revoke IP] [--port N]",
515
525
  "program": "agentos",
516
526
  "area": "Admin",
517
- "description": "open the control panel's port (allowlisted IPs, or any source with --any)",
527
+ "description": "limit the control panel's port to the allowlisted IPs",
518
528
  "valid": true,
519
529
  "doc": "https://github.com/consultancy-outfit/DevOpsBot-SDK/blob/main/docs/COMMANDS.md",
520
530
  "id": "agentos firewall"
@@ -57,6 +57,10 @@
57
57
  },
58
58
  {
59
59
  "issue": "After `agentos-auto update`: \"no systemd unit 'agentos-ui' — restart the panel manually\", \"could not read the firewall state (needs root …) … sudo ufw allow 8765/tcp\", \"VPS health: not reported — not_assigned_vps\"",
60
- "fix": "Not errors. The update restarts the panel so it runs the new code (`sdk/ui/service.py`), checks the panel's port is open for remote access (`sdk/firewall.py` — it never runs sudo itself, so unprivileged it prints the command instead), and reports VPS health only from the VPS assigned to the account (`sdk/vps_health.py`). On a laptop or any machine that is not the assigned VPS all three are expected and need no action; on the VPS, open the port with the printed command if the panel must be reachable remotely, and restart `agentos-ui` if it runs by hand."
60
+ "fix": "Not errors. The update restarts the panel so it runs the new code (`sdk/ui/service.py`), limits the panel's port to the allowed IPs — the backend and the one outside IP; it removes any open-to-everyone rule (`sdk/firewall.py` `ensure_restricted` — it never runs sudo itself, so unprivileged it prints the commands instead), and reports VPS health only from the VPS assigned to the account (`sdk/vps_health.py`). On a laptop or any machine that is not the assigned VPS all three are expected and need no action; on the VPS, run `sudo agentos firewall --apply`, and restart `agentos-ui` if it runs by hand. \"ufw is INACTIVE\" in that output means no rule is enforced at all: `sudo ufw allow OpenSSH && sudo ufw enable`."
61
+ },
62
+ {
63
+ "issue": "Panel at 100% CPU / slow, lots of 403s or unknown IPs in `ss -tn sport = :8765`",
64
+ "fix": "Internet scanners reaching an open 8765. On 1.7.45+ refusals are cheap and capped, but the fix is the firewall: `sudo agentos firewall --apply`, then `sudo ufw status verbose` (8765 → only the allowed IPs; Status active). Older SDKs re-opened the port to everyone on every update — `agentos-auto update` first."
61
65
  }
62
66
  ]
@@ -3,7 +3,7 @@
3
3
  "page": "Dashboard",
4
4
  "file": "index.html",
5
5
  "heading": "Daily ticket activity",
6
- "summary": ""
6
+ "summary": "Bot — waiting for work To Do— In Progress— Done— Backlog— Daily ticket activity 7 days 30 days To Do In Progress Done Backlog Up next0 Remote access Allowed IP: —"
7
7
  },
8
8
  {
9
9
  "page": "Run & Logs",
@@ -1,4 +1,14 @@
1
1
  [
2
+ {
3
+ "pr": 189,
4
+ "date": "2026-10-30",
5
+ "merged_at": "2026-10-30T14:14:39+05:00",
6
+ "type": "Other",
7
+ "area": "General",
8
+ "title": "security: remove public mode; ONE outside IP at a time, set only on the server (allow-ip + Remote access)",
9
+ "version": "1.7.46",
10
+ "url": "https://github.com/consultancy-outfit/DevOpsBot-SDK/pull/189"
11
+ },
2
12
  {
3
13
  "pr": 188,
4
14
  "date": "2026-10-29",
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "version": "1.7.46",
4
+ "commit": "5f00536",
5
+ "pushed_at": "2026-10-30T14:14:39+05:00",
6
+ "generated_at": "2026-10-30T09:15:50+00:00",
7
+ "commands": 59,
8
+ "commands_invalid": 0,
9
+ "commands_undocumented": 0,
10
+ "panel_pages": 11,
11
+ "tests": 2597,
12
+ "prs_7d": 25,
13
+ "prs_30d": 41,
14
+ "features_30d": 12,
15
+ "fixes_30d": 18
16
+ }
17
+ ]