devops-bot-sdk 1.7.45__tar.gz → 1.7.47__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
  4. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/cli.py +18 -7
  6. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/config.py +33 -67
  7. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/faqs.py +10 -18
  8. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/firewall.py +177 -56
  9. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_data/areas.json +13 -4
  10. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_data/commands.json +11 -1
  11. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_data/help.json +5 -1
  12. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_data/pages.json +1 -1
  13. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_data/releases.json +20 -0
  14. devops_bot_sdk-1.7.47/sdk/help_data/snapshot.json +17 -0
  15. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/helpdesk_kb.md +43 -34
  16. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/swagger.py +0 -6
  17. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/api.py +47 -36
  18. devops_bot_sdk-1.7.47/sdk/ui/ip_allowlist.py +199 -0
  19. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/models.py +8 -6
  20. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/server.py +11 -8
  21. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/app.js +44 -0
  22. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/index.html +14 -0
  23. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/styles.css +9 -0
  24. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/updater.py +1 -1
  25. devops_bot_sdk-1.7.47/tests/test_backend_only_allowlist.py +155 -0
  26. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_firewall_open_on_update.py +70 -11
  27. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_swagger_command.py +1 -8
  28. devops_bot_sdk-1.7.47/tests/test_ui_bruteforce_guard.py +94 -0
  29. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ui_gate.py +57 -73
  30. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/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.47}/README.md +0 -0
  35. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  36. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  37. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/requires.txt +0 -0
  38. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  39. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/pyproject.toml +0 -0
  40. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/__init__.py +0 -0
  41. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/classifier.py +0 -0
  42. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/collectors/__init__.py +0 -0
  43. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/collectors/common.py +0 -0
  44. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/collectors/linux_auditd.py +0 -0
  45. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/collectors/macos_eslogger.py +0 -0
  46. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/collectors/windows_eventlog.py +0 -0
  47. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/models.py +0 -0
  48. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/os_setup.py +0 -0
  49. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/recorder.py +0 -0
  50. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/redact.py +0 -0
  51. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/settings.py +0 -0
  52. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/shipper.py +0 -0
  53. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/activity/spool.py +0 -0
  54. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agent_sharing.py +0 -0
  55. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agentd.py +0 -0
  56. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/__init__.py +0 -0
  57. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/activity.py +0 -0
  58. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/builder.py +0 -0
  59. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/chat.py +0 -0
  60. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/connectors.py +0 -0
  61. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/__init__.py +0 -0
  62. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/claude_code.py +0 -0
  63. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/http.py +0 -0
  64. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/mcp.py +0 -0
  65. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/n8n.py +0 -0
  66. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/engines/shell.py +0 -0
  67. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/integrations.py +0 -0
  68. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/inventory.py +0 -0
  69. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/library/__init__.py +0 -0
  70. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/manifest.py +0 -0
  71. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/prompt_studio.py +0 -0
  72. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/prompt_template.py +0 -0
  73. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/prompt_vars.py +0 -0
  74. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/run_log.py +0 -0
  75. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/schedules.py +0 -0
  76. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/secrets.py +0 -0
  77. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/ticket_run.py +0 -0
  78. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/usage.py +0 -0
  79. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/agents/varstore.py +0 -0
  80. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/browser_profile.py +0 -0
  81. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/claude_usage.py +0 -0
  82. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/client.py +0 -0
  83. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/collectors/__init__.py +0 -0
  84. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/collectors/files.py +0 -0
  85. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/collectors/process.py +0 -0
  86. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/collectors/screenshot.py +0 -0
  87. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/crucial.py +0 -0
  88. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/custom_agents.py +0 -0
  89. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/deferred_tickets.py +0 -0
  90. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/departments.py +0 -0
  91. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/__init__.py +0 -0
  92. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/assets.py +0 -0
  93. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/bootstrap.py +0 -0
  94. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/browser.py +0 -0
  95. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/compare.py +0 -0
  96. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/design_verify/loop.py +0 -0
  97. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/estimator.py +0 -0
  98. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/exceptions.py +0 -0
  99. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/fallback_handoff.py +0 -0
  100. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/formats.py +0 -0
  101. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/git_ops.py +0 -0
  102. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/github_fetch.py +0 -0
  103. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/github_ingest.py +0 -0
  104. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/graph_recon.py +0 -0
  105. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/graphify.py +0 -0
  106. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/help_actions.py +0 -0
  107. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/helpdesk.py +0 -0
  108. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/hooks/__init__.py +0 -0
  109. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/hooks/crucial_guard.py +0 -0
  110. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/instructions_template.py +0 -0
  111. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/intake.py +0 -0
  112. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ipc/__init__.py +0 -0
  113. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ipc/electron_bridge.py +0 -0
  114. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ipc/handlers.py +0 -0
  115. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/journal.py +0 -0
  116. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/local_exec.py +0 -0
  117. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/log_colors.py +0 -0
  118. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/memory_guard.py +0 -0
  119. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/model_policy.py +0 -0
  120. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/models/__init__.py +0 -0
  121. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/models/envelope.py +0 -0
  122. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/models/requests.py +0 -0
  123. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/models/responses.py +0 -0
  124. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/models/snapshots.py +0 -0
  125. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/phase_timer.py +0 -0
  126. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/plan_run.py +0 -0
  127. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/platform_compat.py +0 -0
  128. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/prd_progress.py +0 -0
  129. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/proof_readers.py +0 -0
  130. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/providers.py +0 -0
  131. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/py.typed +0 -0
  132. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/resume.py +0 -0
  133. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/router.py +0 -0
  134. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/run_auto.py +0 -0
  135. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/schedule_tickets.py +0 -0
  136. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/schedule_timeline.py +0 -0
  137. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/scheduler.py +0 -0
  138. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/scope_plan.py +0 -0
  139. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/self_report.py +0 -0
  140. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/self_report_local.py +0 -0
  141. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/sse.py +0 -0
  142. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/task_file.py +0 -0
  143. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/task_types.py +0 -0
  144. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/test.py +0 -0
  145. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/test_env.py +0 -0
  146. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/test_pipeline.py +0 -0
  147. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ticket_board.py +0 -0
  148. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ticket_bulk.py +0 -0
  149. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ticket_fields.py +0 -0
  150. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ticket_plan.py +0 -0
  151. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ticket_pools.py +0 -0
  152. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/tickets.py +0 -0
  153. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/tiers/__init__.py +0 -0
  154. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/tiers/mini_agent.py +0 -0
  155. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/__init__.py +0 -0
  156. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/dashboard.py +0 -0
  157. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/health.py +0 -0
  158. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/jobs.py +0 -0
  159. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/runner.py +0 -0
  160. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/security.py +0 -0
  161. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/service.py +0 -0
  162. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/session.py +0 -0
  163. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/setup_manager.py +0 -0
  164. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/agents.html +0 -0
  165. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/chat.html +0 -0
  166. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/custom-agents.html +0 -0
  167. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/default-agents.html +0 -0
  168. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/faqs.html +0 -0
  169. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/help.html +0 -0
  170. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/report.html +0 -0
  171. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/runs.html +0 -0
  172. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/schedules.html +0 -0
  173. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/setup.html +0 -0
  174. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/static/tickets.html +0 -0
  175. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/ui/swagger.py +0 -0
  176. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/url_access.py +0 -0
  177. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/vps_health.py +0 -0
  178. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/vps_identity.py +0 -0
  179. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/sdk/write_hook.py +0 -0
  180. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/setup.cfg +0 -0
  181. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_claude_runs.py +0 -0
  182. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_collectors.py +0 -0
  183. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_core.py +0 -0
  184. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_live_tracking.py +0 -0
  185. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_macos.py +0 -0
  186. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_os_setup.py +0 -0
  187. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_activity_pipeline.py +0 -0
  188. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_activity.py +0 -0
  189. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_activity_tracking.py +0 -0
  190. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_chat_features.py +0 -0
  191. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_crud_cli.py +0 -0
  192. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_listing.py +0 -0
  193. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_prompt_async.py +0 -0
  194. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_prompt_models.py +0 -0
  195. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_run_ticket.py +0 -0
  196. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_schedules.py +0 -0
  197. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_sharing.py +0 -0
  198. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agent_workspace_settings.py +0 -0
  199. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_agents_connectors_builder.py +0 -0
  200. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_allowed_tools_config.py +0 -0
  201. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_approval_park.py +0 -0
  202. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_auto_agent_dispatch.py +0 -0
  203. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_auto_ticket_queue.py +0 -0
  204. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_browser_profile.py +0 -0
  205. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_cheap_tier.py +0 -0
  206. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_claude_usage_gate.py +0 -0
  207. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_client_jira_projects.py +0 -0
  208. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_config_token_diagnostics.py +0 -0
  209. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_configure_token_owner.py +0 -0
  210. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_console_encoding.py +0 -0
  211. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_crash_recovery.py +0 -0
  212. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_create_safe_retry.py +0 -0
  213. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_deferred_tickets.py +0 -0
  214. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_departments.py +0 -0
  215. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_done_transition.py +0 -0
  216. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_epic_decompose_run.py +0 -0
  217. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_epic_inherited_agent_id.py +0 -0
  218. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_epic_session_isolation.py +0 -0
  219. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_estimator.py +0 -0
  220. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_fallback_cli.py +0 -0
  221. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_fallback_cli_upgrade.py +0 -0
  222. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_fallback_rate_limit.py +0 -0
  223. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_faqs.py +0 -0
  224. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_formats.py +0 -0
  225. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_git_action.py +0 -0
  226. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_github_fetch.py +0 -0
  227. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_github_ingest.py +0 -0
  228. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_graph_off_by_default.py +0 -0
  229. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_graph_recon.py +0 -0
  230. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_graphify_build.py +0 -0
  231. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_graphify_hook_install.py +0 -0
  232. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_health.py +0 -0
  233. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_help_actions.py +0 -0
  234. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_help_attachments.py +0 -0
  235. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_help_questions.py +0 -0
  236. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_helpdesk.py +0 -0
  237. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_infographic_data.py +0 -0
  238. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_instructions_template.py +0 -0
  239. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_intake_job_outcome.py +0 -0
  240. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_intake_plan_review.py +0 -0
  241. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_journal.py +0 -0
  242. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_limit_fallback_handoff.py +0 -0
  243. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_local_exec_provider.py +0 -0
  244. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_log_colors_backlog.py +0 -0
  245. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_mcp_route_contract.py +0 -0
  246. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_memory_guard.py +0 -0
  247. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_model_policy.py +0 -0
  248. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_panel_help.py +0 -0
  249. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_panel_ticket_schedules.py +0 -0
  250. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_parallel_tickets.py +0 -0
  251. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_phase_timer.py +0 -0
  252. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_plan_ahead.py +0 -0
  253. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_plan_new_command.py +0 -0
  254. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_plan_run_activity.py +0 -0
  255. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_pool_pr_modes.py +0 -0
  256. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_pr_identity.py +0 -0
  257. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_prd_artifact.py +0 -0
  258. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_project_path_resolution.py +0 -0
  259. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_proof_readers.py +0 -0
  260. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_providers.py +0 -0
  261. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_quiet_run_log.py +0 -0
  262. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_recon_session_isolation.py +0 -0
  263. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_resume.py +0 -0
  264. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_router.py +0 -0
  265. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_run_auto_agent_only.py +0 -0
  266. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_run_outputs.py +0 -0
  267. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_run_verdict_is_honest.py +0 -0
  268. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_schedule_api.py +0 -0
  269. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_schedule_outputs_and_plans.py +0 -0
  270. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_schedule_tickets.py +0 -0
  271. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_schedule_timeline.py +0 -0
  272. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_scheduler.py +0 -0
  273. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_scheduler_transient_retry.py +0 -0
  274. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_self_report.py +0 -0
  275. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_service_token_warning.py +0 -0
  276. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_stale_panel_restart.py +0 -0
  277. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_task_file_and_plan_run.py +0 -0
  278. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_task_file_format_doc.py +0 -0
  279. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_theme_switch.py +0 -0
  280. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_board.py +0 -0
  281. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_bulk.py +0 -0
  282. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_cli.py +0 -0
  283. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_detail_view.py +0 -0
  284. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_fields.py +0 -0
  285. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_plan.py +0 -0
  286. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_pools.py +0 -0
  287. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_run_resolve_by_id.py +0 -0
  288. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_schedule_and_agent.py +0 -0
  289. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ticket_speed_fixes.py +0 -0
  290. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_tier_flag_parity.py +0 -0
  291. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_ui_no_native_dialogs.py +0 -0
  292. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_update_timer.py +0 -0
  293. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_updater_extras.py +0 -0
  294. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_updater_ignores_cwd.py +0 -0
  295. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_updater_pep668.py +0 -0
  296. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_updater_reports_the_truth.py +0 -0
  297. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_url_access.py +0 -0
  298. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_version_flag.py +0 -0
  299. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_vps_health.py +0 -0
  300. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_vps_only_reporting.py +0 -0
  301. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_windows_loop_identity.py +0 -0
  302. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_windows_panel_task.py +0 -0
  303. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_windows_stop_and_pidfile.py +0 -0
  304. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_windows_task_paths.py +0 -0
  305. {devops_bot_sdk-1.7.45 → devops_bot_sdk-1.7.47}/tests/test_write_hook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.7.45
3
+ Version: 1.7.47
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.7.45
3
+ Version: 1.7.47
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -190,6 +190,7 @@ tests/test_allowed_tools_config.py
190
190
  tests/test_approval_park.py
191
191
  tests/test_auto_agent_dispatch.py
192
192
  tests/test_auto_ticket_queue.py
193
+ tests/test_backend_only_allowlist.py
193
194
  tests/test_browser_profile.py
194
195
  tests/test_cheap_tier.py
195
196
  tests/test_claude_usage_gate.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.47
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -34,7 +34,7 @@ Rules:
34
34
  - All data egress through submit_webhook only
35
35
  """
36
36
 
37
- __version__ = "1.7.45" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.7.47" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
38
38
  __author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
39
39
 
40
40
  from sdk.client import BackendClient
@@ -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 [--clear]
113
+ Show who may reach the panel: this machine and the DevOps Bot backend only
114
+ — no outside IP (since 1.7.47). `agentos allow-ip <IP>` is refused (exit 2)
115
+ with the SSH-tunnel alternative (ssh -L 8765:127.0.0.1:8765 <user>@<server>).
116
+ `--clear` removes outside IPs an older SDK left in config.toml and syncs the
117
+ host firewall (that 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 [--clear] # who may reach the panel (no outside IP; use an SSH tunnel)")
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,39 @@ 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/*``).
231
-
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.
230
+ """Client IPs permitted to reach the ``agentos-ui`` server: the built-in backend
231
+ IP(s) and nothing else.
232
+
233
+ No outside user IP is allowed (since 1.7.47 — 1.7.46 allowed one). The panel is
234
+ for this machine and the DevOps Bot backend: open it on the server, or from
235
+ another computer through an SSH tunnel (``ssh -L 8765:127.0.0.1:8765 …``),
236
+ which arrives as loopback. Loopback and this host's own IPs are always trusted
237
+ separately. Anything still configured in ``AGENTOS_UI_ALLOWED_IPS`` or
238
+ config.toml ``allowed_ips`` is ignored and reported by
239
+ :func:`ignored_allowed_ips`; the host firewall (`sdk.firewall.ensure_restricted`)
240
+ removes its rule.
241
+ """
242
+ return list(_DEFAULT_ALLOWED_IPS)
235
243
 
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
244
 
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.
245
- """
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
- ]
245
+ def _configured_user_ips() -> list[str]:
246
+ """Every non-backend entry configured anywhere, oldest source first:
247
+ ``.env``, process env, then config.toml (the runtime / API-added one)."""
252
248
  out: list[str] = []
253
- for src in sources:
249
+ for src in (_dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
250
+ os.environ.get("AGENTOS_UI_ALLOWED_IPS", ""),
251
+ _load_raw().get("allowed_ips", "")):
254
252
  for ip in _parse_ip_list(src):
255
- if ip not in out:
253
+ if ip not in _DEFAULT_ALLOWED_IPS and ip not in out:
256
254
  out.append(ip)
257
255
  return out
258
256
 
259
257
 
258
+ def ignored_allowed_ips() -> list[str]:
259
+ """Configured outside IPs/ranges that are NOT allowed — none ever is."""
260
+ return _configured_user_ips()
261
+
262
+
260
263
  # Built-in baseline of tools a headless agent run may use. Kept here (not in
261
264
  # local_exec) so it is one configurable setting rather than a literal buried in the
262
265
  # spawn code: the set WILL grow, and a new tool must be addable without editing
@@ -311,26 +314,14 @@ class PermanentAllowlistEntry(ValueError):
311
314
 
312
315
 
313
316
  def permanent_allowed_ips() -> list[str]:
314
- """IPs/CIDRs whitelisted **permanently** — from ``.env`` (``~/.agentos/.env`` or
315
- CWD) or the ``AGENTOS_UI_ALLOWED_IPS`` process env var.
316
-
317
- These are the fixed baseline: they are ALWAYS allowed and **cannot be disabled**
318
- through the API — change them by editing ``.env`` and restarting.
319
- """
320
- out: list[str] = []
321
- for src in (",".join(_DEFAULT_ALLOWED_IPS), # built-in backend IP(s) — always permanent
322
- _dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
323
- os.environ.get("AGENTOS_UI_ALLOWED_IPS", "")):
324
- for ip in _parse_ip_list(src):
325
- if ip not in out:
326
- out.append(ip)
327
- return out
317
+ """The IPs that are always allowed and cannot be disabled — the built-in
318
+ backend IP(s). ``AGENTOS_UI_ALLOWED_IPS`` no longer adds to them (1.7.47)."""
319
+ return list(_DEFAULT_ALLOWED_IPS)
328
320
 
329
321
 
330
322
  def runtime_allowed_ips() -> list[str]:
331
- """IPs/CIDRs whitelisted **at runtime** (config.toml ``allowed_ips``) — added by
332
- the whitelist endpoint and removable by the disable endpoint. They stay in the
333
- allowlist until disabled."""
323
+ """IPs/CIDRs left in config.toml ``allowed_ips`` by an older SDK. Ignored since
324
+ 1.7.47 (see :func:`get_allowed_ips`); ``agentos allow-ip --clear`` removes them."""
334
325
  return _parse_ip_list(_load_raw().get("allowed_ips", ""))
335
326
 
336
327
 
@@ -433,40 +424,15 @@ def journal_enabled() -> bool:
433
424
  return flag(FLAG_JOURNAL) or tiered_routing_enabled()
434
425
 
435
426
 
436
- def add_allowed_ip(ip: str) -> list[str]:
437
- """Whitelist an IP/CIDR at RUNTIME (persisted to config.toml ``allowed_ips``).
438
-
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.
443
- """
444
- ip = (ip or "").strip()
445
- if not ip:
446
- raise ValueError("ip is required")
447
- if ip in permanent_allowed_ips(): # already permanent — nothing to add
448
- return get_allowed_ips()
449
- 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)
454
- _save_raw(raw)
455
- return get_allowed_ips()
456
-
457
-
458
427
  def remove_allowed_ip(ip: str) -> list[str]:
459
- """Disable a RUNTIME-whitelisted IP/CIDR (remove from config.toml ``allowed_ips``).
428
+ """Remove a leftover entry from config.toml ``allowed_ips`` (ignored anyway).
460
429
 
461
- Raises :class:`PermanentAllowlistEntry` if the IP is permanently whitelisted via
462
- ``.env`` — those are never removable through the API (edit ``.env`` instead).
463
- Returns the merged allowlist.
430
+ Raises :class:`PermanentAllowlistEntry` for a backend IP. Returns the allowlist.
464
431
  """
465
432
  ip = (ip or "").strip()
466
433
  if ip in permanent_allowed_ips():
467
434
  raise PermanentAllowlistEntry(
468
- f"{ip} is permanently whitelisted via .env (AGENTOS_UI_ALLOWED_IPS) — "
469
- "it cannot be disabled through the API; edit .env and restart to change it."
435
+ f"{ip} is a DevOps Bot backend IP — it is always allowed and cannot be removed."
470
436
  )
471
437
  raw = _load_raw()
472
438
  entries = [x for x in _parse_ip_list(raw.get("allowed_ips", "")) if x != ip]
@@ -19,24 +19,16 @@ FAQS: list[tuple[str, str, str]] = [
19
19
  "7. **Your co_ API token** — `agentos configure`\n"
20
20
  "Then `agentos doctor` checks the lot."),
21
21
  ("Basics", "How do I open the control panel from another machine?",
22
- "**Easiest — an SSH tunnel (nothing to open):** on your machine run "
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."),
22
+ "**Use an SSH tunnel** — no outside IP can be allowed; only the server itself and the DevOps Bot "
23
+ "backend reach the panel.\n"
24
+ "1. **Linux / macOS:** on your machine run `ssh -L 8765:127.0.0.1:8765 you@SERVER` and leave it open.\n"
25
+ "2. **Windows:** the same command in PowerShell (built-in OpenSSH client): "
26
+ "`ssh -L 8765:127.0.0.1:8765 you@SERVER`.\n"
27
+ "3. Browse `http://127.0.0.1:8765` on your machine — it arrives as loopback, so no IP to allow. "
28
+ "(Port 8765 busy locally? `ssh -L 9000:127.0.0.1:8765 …` and use `http://127.0.0.1:9000`.)\n"
29
+ "Or open `http://127.0.0.1:8765` on the server itself. `agentos allow-ip <IP>` is refused; "
30
+ "`agentos allow-ip` shows who may connect and `agentos allow-ip --clear` removes outside IPs an older "
31
+ "SDK left behind. A leaked token cannot open the panel to another address."),
40
32
  ("Basics", "How do I use devops-bot-sdk on another machine?",
41
33
  "Install it there too — the same checklist (Python, git, SSH key, repo access, claude or ad), then "
42
34
  "`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
  """
@@ -159,6 +158,41 @@ def revoke_any_rule(port: int, system: str | None = None) -> str:
159
158
  return f"# unsupported platform {sysname!r}"
160
159
 
161
160
 
161
+ def revoke_any_rules(port: int, system: str | None = None,
162
+ forms: list[str] | None = None) -> list[str]:
163
+ """Every command needed to remove the open-to-any rule(s) for ``port``.
164
+
165
+ ufw keeps ``8765/tcp`` and a bare ``8765`` (tcp+udp, what a hand-typed
166
+ ``ufw allow 8765`` makes) as DIFFERENT rules, and deleting one leaves the other
167
+ — the port then stays open to everyone after ``ufw delete allow 8765/tcp``.
168
+ ``forms`` are the ``To`` spellings ufw shows for the Anywhere rows; unknown
169
+ (None) → both, since deleting a rule that does not exist is harmless.
170
+ """
171
+ sysname = (system or _system()).lower()
172
+ if sysname != "linux":
173
+ return [revoke_any_rule(port, sysname)]
174
+ if forms is None:
175
+ forms = [f"{port}/tcp", str(port)]
176
+ return [f"sudo ufw delete allow {f}" for f in forms]
177
+
178
+
179
+ def _ufw_any_forms(port: int) -> list[str] | None:
180
+ """The ``To`` spellings of the IPv4 open-to-any rows for ``port`` (e.g.
181
+ ``["8765/tcp", "8765"]``), or None when ufw cannot be read."""
182
+ text = _run(_ufw_argv() + ["status"])
183
+ if text is None:
184
+ return None
185
+ out: list[str] = []
186
+ for line in text.splitlines():
187
+ parts = line.split("#", 1)[0].split()
188
+ if (len(parts) >= 3 and "(v6)" not in line and parts[1].upper() == "ALLOW"
189
+ and parts[-1] == "Anywhere"
190
+ and (parts[0] == str(port) or parts[0].startswith(f"{port}/"))
191
+ and parts[0] not in out):
192
+ out.append(parts[0])
193
+ return out
194
+
195
+
162
196
  def ensure_open(port: int | None = None, *, apply: bool = True) -> tuple[bool, str]:
163
197
  """Make sure ``port`` is reachable. Returns ``(changed, message)``. Never raises.
164
198
 
@@ -198,7 +232,7 @@ def ensure_open(port: int | None = None, *, apply: bool = True) -> tuple[bool, s
198
232
  return (False, f"macOS: pf state is not readable and pf rules are file-based —\n{printable}")
199
233
  if sources is None:
200
234
  ok, tool = _tool_present()
201
- why = tool if not ok else "needs root to read the firewall state"
235
+ why = tool if not ok else "needs root or passwordless sudo to read the firewall state"
202
236
  return (False, f"could not read the firewall state ({why}) — to open it yourself:\n{printable}")
203
237
 
204
238
  # Readable and genuinely empty for this port.
@@ -215,13 +249,16 @@ def ensure_open(port: int | None = None, *, apply: bool = True) -> tuple[bool, s
215
249
 
216
250
 
217
251
  def _apply_rules(rules: list[str]) -> list[str]:
218
- """Run each rule (already privileged, so a printed ``sudo `` is stripped).
252
+ """Run each rule. As root a printed ``sudo `` is stripped; otherwise it becomes
253
+ ``sudo -n `` (passwordless sudo — see :func:`_sudo_n_ok`), which fails instead
254
+ of waiting for a password.
219
255
 
220
256
  Returns one ``"<cmd>: <reason>"`` per failure — empty means all applied.
221
257
  """
258
+ prefix = "sudo -n " if _sudo_n_ok() else ""
222
259
  failed: list[str] = []
223
260
  for rule in rules:
224
- cmd = rule[len("sudo "):] if rule.startswith("sudo ") else rule
261
+ cmd = prefix + rule[len("sudo "):] if rule.startswith("sudo ") else rule
225
262
  try:
226
263
  proc = subprocess.run(
227
264
  cmd, shell=True, capture_output=True, text=True, timeout=30,
@@ -246,24 +283,27 @@ def _net(value: str):
246
283
  def open_any_opted_in() -> bool:
247
284
  """Has the operator chosen to keep the port open to any source?
248
285
 
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.
286
+ Only an explicit ``AGENTOS_FIREWALL_OPEN_ANY=1`` — read from the process env
287
+ or ``~/.agentos/.env``, like every other ``AGENTOS_UI_*`` toggle. (The removed
288
+ ``AGENTOS_UI_PUBLIC=1`` used to imply it.)
252
289
  """
253
290
  try:
254
- from sdk.ui.ip_allowlist import _TRUE, _setting, public_mode
291
+ from sdk.ui.ip_allowlist import _TRUE, _setting
255
292
  except Exception: # noqa: BLE001 — never fail open on an import hiccup
256
293
  return False
257
- return public_mode() or _setting("AGENTOS_FIREWALL_OPEN_ANY").lower() in _TRUE
294
+ return _setting("AGENTOS_FIREWALL_OPEN_ANY").lower() in _TRUE
258
295
 
259
296
 
260
297
  def ensure_restricted(port: int | None = None, *, apply: bool = True,
261
298
  ips: list[str] | None = None) -> tuple[bool, str]:
262
- """Limit ``port`` to the allowlisted IPs. Returns ``(changed, message)``. Never raises.
299
+ """Limit ``port`` to EXACTLY the allowlisted IPs. Returns ``(changed, message)``.
300
+ Never raises.
263
301
 
264
302
  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
303
+ the open-to-any rule and every per-IP rule for an address no longer allowed
304
+ (only one user IP may reach the panel, so replacing it must close the old
305
+ one) — in that order, and the removals are skipped if any add failed, so the
306
+ backend never loses its route mid-change. Same contract as
267
307
  :func:`ensure_open` otherwise: unreadable state is never treated as closed,
268
308
  an unprivileged run only returns the commands, and macOS never applies.
269
309
 
@@ -291,26 +331,35 @@ def ensure_restricted(port: int | None = None, *, apply: bool = True,
291
331
  " sudo ufw allow OpenSSH && sudo ufw enable")
292
332
 
293
333
  if sources is None:
294
- rules = allow_rules(ips, port, sysname) + [revoke_any_rule(port, sysname)]
334
+ rules = allow_rules(ips, port, sysname) + revoke_any_rules(port, sysname)
295
335
  printable = " " + "\n ".join(rules)
296
336
  ok, tool = _tool_present()
297
- why = tool if not ok else "needs root to read the firewall state"
337
+ why = tool if not ok else "needs root or passwordless sudo to read the firewall state"
298
338
  return (False, f"could not read the firewall state ({why}) — to restrict TCP {port} "
299
339
  f"to the allowlist yourself:\n{printable}")
300
340
 
301
341
  have = {n for n in (_net(x) for x in sources) if n is not None}
342
+ want = {n for n in (_net(ip) for ip in ips) if n is not None}
302
343
  missing = [ip for ip in ips if _net(ip) is not None and _net(ip) not in have]
344
+ stale = sorted(x for x in sources if _net(x) is not None and _net(x) not in want)
303
345
  open_any = "Anywhere" in sources
304
346
 
305
- if not missing and not open_any:
347
+ if not missing and not open_any and not stale:
306
348
  return (False, f"TCP {port} is restricted to the allowlist" + inactive_note)
307
349
 
308
- rules = allow_rules(missing, port, sysname)
309
- if open_any:
310
- rules.append(revoke_any_rule(port, sysname))
350
+ # Read ufw's exact spelling only when the deletes will run; a printed list
351
+ # names both forms (deleting a rule that is not there is harmless).
352
+ any_forms = (_ufw_any_forms(port) or [f"{port}/tcp"]) if (
353
+ sysname == "linux" and open_any and apply and _is_privileged()) else None
354
+ removals = (revoke_any_rules(port, sysname, any_forms) if open_any else []) + [
355
+ revoke_rule(x, port, sysname) for x in stale]
356
+ rules = allow_rules(missing, port, sysname) + removals
311
357
  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)}")
358
+ what = "; ".join(w for w in (
359
+ f"TCP {port} is open to ANY source" if open_any else "",
360
+ f"missing a rule for: {', '.join(missing)}" if missing else "",
361
+ f"still allows: {', '.join(stale)}" if stale else "",
362
+ ) if w)
314
363
 
315
364
  if not apply or sysname == "darwin":
316
365
  return (False, f"{what}. To restrict it to the allowlist:\n{printable}" + inactive_note)
@@ -323,38 +372,31 @@ def ensure_restricted(port: int | None = None, *, apply: bool = True,
323
372
  # The any-rule stays: removing it now could cut the backend off.
324
373
  return (False, f"could not add allowlist rules for TCP {port}: {'; '.join(failed)}"
325
374
  f"\nThe open-to-any rule was left in place.")
326
- if open_any:
327
- failed = _apply_rules([revoke_any_rule(port, sysname)])
375
+ if removals:
376
+ _apply_rules(removals)
328
377
  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.
378
+ left = sorted(x for x in still
379
+ if x == "Anywhere" or (_net(x) is not None and _net(x) not in want))
380
+ if left:
381
+ return (True, f"TCP {port} still allows {', '.join(left)} — a rule the SDK did "
382
+ f"not create. Find and delete it:\n "
383
+ + ("sudo ufw status numbered" if sysname == "linux" else
384
+ "netsh advfirewall firewall show rule name=all dir=in")
385
+ + inactive_note)
386
+ return (True, f"restricted TCP {port} to the allowlist: {', '.join(ips)}" + inactive_note)
387
+
388
+
389
+ def sync_rules(port: int | None = None) -> tuple[bool, str]:
390
+ """Bring the host firewall in line with the allowlist after it changed.
391
+
392
+ Called when the one user IP is set or cleared (CLI or panel), so the new IP's
393
+ rule is added and the old one's removed straight away rather than at the next
394
+ self-update. Leaves a deliberately open port alone. Never raises.
346
395
  """
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
396
  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}")
397
+ return (False, "port is open to any source by choice (AGENTOS_FIREWALL_OPEN_ANY) — "
398
+ "no per-IP rules to change")
399
+ return ensure_restricted(port)
358
400
 
359
401
 
360
402
  def revoke_rule(ip: str, port: int, system: str | None = None) -> str:
@@ -467,7 +509,7 @@ def port_sources(port: int, system: str | None = None, *,
467
509
  """
468
510
  sysname = (system or _system()).lower()
469
511
  if sysname == "linux":
470
- text = _run(["ufw", "status"])
512
+ text = _run(_ufw_argv() + ["status"])
471
513
  if text is None:
472
514
  return None
473
515
  got = _parse_ufw(text, port)
@@ -488,10 +530,42 @@ def _ufw_inactive() -> bool:
488
530
  Then every ufw rule is decoration and the port is open regardless — the
489
531
  "per-IP rules are applied but the flood still arrives" case.
490
532
  """
491
- text = _run(["ufw", "status"])
533
+ text = _run(_ufw_argv() + ["status"])
492
534
  return bool(text) and "status: inactive" in text.lower()
493
535
 
494
536
 
537
+ def _is_root() -> bool:
538
+ return hasattr(os, "geteuid") and os.geteuid() == 0
539
+
540
+
541
+ _SUDO_N: bool | None = None
542
+
543
+
544
+ def _sudo_n_ok() -> bool:
545
+ """True when this (non-root, Linux) user may run ufw through ``sudo -n``.
546
+
547
+ ``agentos-auto update`` runs as the panel user, not root, so without this the
548
+ post-update hook could neither read nor fix ufw and only ever printed the
549
+ commands — the port stayed open to the world between updates. ``-n`` never
550
+ prompts, so an unattended update still cannot stall waiting for a password.
551
+ Probed once per process.
552
+ """
553
+ global _SUDO_N
554
+ if _SUDO_N is None:
555
+ _SUDO_N = False
556
+ if _system() == "linux" and not _is_root() and shutil.which("sudo") and shutil.which("ufw"):
557
+ try:
558
+ _SUDO_N = subprocess.run(["sudo", "-n", "ufw", "status"], capture_output=True,
559
+ text=True, timeout=10).returncode == 0
560
+ except Exception: # noqa: BLE001
561
+ _SUDO_N = False
562
+ return _SUDO_N
563
+
564
+
565
+ def _ufw_argv() -> list[str]:
566
+ return ["sudo", "-n", "ufw"] if _sudo_n_ok() else ["ufw"]
567
+
568
+
495
569
  def _is_privileged() -> bool:
496
570
  if _system() == "windows":
497
571
  try:
@@ -499,7 +573,7 @@ def _is_privileged() -> bool:
499
573
  return bool(ctypes.windll.shell32.IsUserAnAdmin())
500
574
  except Exception: # noqa: BLE001 — not Windows, or no ctypes
501
575
  return False
502
- return hasattr(os, "geteuid") and os.geteuid() == 0
576
+ return _is_root() or _sudo_n_ok()
503
577
 
504
578
 
505
579
  def cli(argv: list[str]) -> int:
@@ -572,7 +646,8 @@ def cli(argv: list[str]) -> int:
572
646
 
573
647
  rules = allow_rules(ips, port, sysname)
574
648
  if "Anywhere" in (port_sources(port, sysname) or set()):
575
- rules.append(revoke_any_rule(port, sysname))
649
+ rules.extend(revoke_any_rules(
650
+ port, sysname, _ufw_any_forms(port) if sysname == "linux" else None))
576
651
  print(f"\nTCP {port} is currently open to ANY source — the last rule removes that.")
577
652
  print("\nFirewall rules for this host:\n")
578
653
  for r in rules:
@@ -607,5 +682,51 @@ def cli(argv: list[str]) -> int:
607
682
  return 0 if changed or "restricted to the allowlist" in msg else 1
608
683
 
609
684
 
685
+ def allow_ip_cli(argv: list[str]) -> int:
686
+ """``agentos allow-ip [--clear]`` — show who may reach the panel, or clear the
687
+ outside IPs an older SDK left configured.
688
+
689
+ Since 1.7.47 no outside IP is allowed — only this machine and the DevOps Bot
690
+ backend — so setting one is refused with the SSH-tunnel alternative.
691
+ ``--clear`` empties config.toml ``allowed_ips`` and syncs the host firewall
692
+ (which drops their rules); ``.env`` entries are reported for the operator to
693
+ delete.
694
+ """
695
+ from sdk import config
696
+
697
+ def show() -> None:
698
+ print(f"Allowed : this machine + DevOps Bot backend "
699
+ f"({', '.join(config._DEFAULT_ALLOWED_IPS)}) — no outside IP")
700
+ ignored = config.ignored_allowed_ips()
701
+ if ignored:
702
+ print(f"Ignored : {', '.join(ignored)} (outside IPs are not allowed; "
703
+ "`agentos allow-ip --clear` removes them)")
704
+
705
+ args = [a for a in argv if a.strip()]
706
+ if not args or args[0] in ("--show", "show"):
707
+ show()
708
+ return 0
709
+
710
+ if args[0] not in ("--clear", "clear", "--remove"):
711
+ print("error: outside IPs can no longer be allowed — only this machine and the "
712
+ "DevOps Bot backend reach the panel.\n"
713
+ "From another computer use an SSH tunnel:\n"
714
+ " ssh -L 8765:127.0.0.1:8765 <user>@<server> then open http://127.0.0.1:8765")
715
+ return 2
716
+
717
+ for ip in config.runtime_allowed_ips():
718
+ if ip not in config._DEFAULT_ALLOWED_IPS:
719
+ config.remove_allowed_ip(ip)
720
+ print(f"✓ removed {ip} from ~/.agentos/config.toml")
721
+ left = config.ignored_allowed_ips()
722
+ if left:
723
+ print(f"Still in AGENTOS_UI_ALLOWED_IPS (~/.agentos/.env or the environment): "
724
+ f"{', '.join(left)} — ignored; delete that line to tidy up.")
725
+ changed, msg = sync_rules()
726
+ print(("↻ " if changed else "") + msg)
727
+ show()
728
+ return 0
729
+
730
+
610
731
  if __name__ == "__main__": # pragma: no cover
611
732
  sys.exit(cli(sys.argv[1:]))