devops-bot-sdk 1.6.34__tar.gz → 1.6.37__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 (190) hide show
  1. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/SOURCES.txt +5 -2
  4. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/entry_points.txt +1 -0
  5. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/pyproject.toml +1 -0
  6. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/__init__.py +2 -2
  7. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/cli.py +31 -1
  8. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/client.py +65 -38
  9. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/config.py +9 -1
  10. devops_bot_sdk-1.6.37/sdk/firewall.py +310 -0
  11. devops_bot_sdk-1.6.37/sdk/github_ingest.py +573 -0
  12. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/run_auto.py +17 -24
  13. devops_bot_sdk-1.6.37/sdk/swagger.py +356 -0
  14. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/api.py +0 -27
  15. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/ip_allowlist.py +43 -16
  16. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/models.py +0 -13
  17. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/server.py +125 -17
  18. devops_bot_sdk-1.6.37/sdk/vps_health.py +264 -0
  19. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_github_ingest.py +8 -4
  20. devops_bot_sdk-1.6.37/tests/test_swagger_command.py +260 -0
  21. devops_bot_sdk-1.6.37/tests/test_ui_gate.py +281 -0
  22. devops_bot_sdk-1.6.37/tests/test_ui_lifespan.py +89 -0
  23. devops_bot_sdk-1.6.34/sdk/github_ingest.py +0 -161
  24. devops_bot_sdk-1.6.34/sdk/ui/static/login.html +0 -111
  25. devops_bot_sdk-1.6.34/sdk/vps_health.py +0 -111
  26. devops_bot_sdk-1.6.34/tests/test_otp_login.py +0 -178
  27. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/README.md +0 -0
  28. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  29. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/requires.txt +0 -0
  30. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  31. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agentd.py +0 -0
  32. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/__init__.py +0 -0
  33. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/activity.py +0 -0
  34. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/builder.py +0 -0
  35. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/chat.py +0 -0
  36. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/connectors.py +0 -0
  37. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/__init__.py +0 -0
  38. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/claude_code.py +0 -0
  39. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/http.py +0 -0
  40. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/mcp.py +0 -0
  41. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/n8n.py +0 -0
  42. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/engines/shell.py +0 -0
  43. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/integrations.py +0 -0
  44. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/inventory.py +0 -0
  45. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/library/__init__.py +0 -0
  46. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/manifest.py +0 -0
  47. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_studio.py +0 -0
  48. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_template.py +0 -0
  49. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/prompt_vars.py +0 -0
  50. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/run_log.py +0 -0
  51. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/schedules.py +0 -0
  52. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/secrets.py +0 -0
  53. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/ticket_run.py +0 -0
  54. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/usage.py +0 -0
  55. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/agents/varstore.py +0 -0
  56. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/browser_profile.py +0 -0
  57. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/collectors/__init__.py +0 -0
  58. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/collectors/files.py +0 -0
  59. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/collectors/process.py +0 -0
  60. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/collectors/screenshot.py +0 -0
  61. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/crucial.py +0 -0
  62. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/departments.py +0 -0
  63. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/__init__.py +0 -0
  64. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/assets.py +0 -0
  65. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/bootstrap.py +0 -0
  66. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/browser.py +0 -0
  67. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/compare.py +0 -0
  68. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/design_verify/loop.py +0 -0
  69. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/estimator.py +0 -0
  70. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/exceptions.py +0 -0
  71. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/formats.py +0 -0
  72. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/git_ops.py +0 -0
  73. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/graph_recon.py +0 -0
  74. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/graphify.py +0 -0
  75. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/hooks/__init__.py +0 -0
  76. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/hooks/crucial_guard.py +0 -0
  77. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/intake.py +0 -0
  78. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ipc/__init__.py +0 -0
  79. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ipc/electron_bridge.py +0 -0
  80. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ipc/handlers.py +0 -0
  81. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/journal.py +0 -0
  82. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/local_exec.py +0 -0
  83. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/models/__init__.py +0 -0
  84. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/models/envelope.py +0 -0
  85. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/models/requests.py +0 -0
  86. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/models/responses.py +0 -0
  87. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/models/snapshots.py +0 -0
  88. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/phase_timer.py +0 -0
  89. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/plan_run.py +0 -0
  90. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/platform_compat.py +0 -0
  91. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/prd_progress.py +0 -0
  92. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/proof_readers.py +0 -0
  93. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/providers.py +0 -0
  94. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/py.typed +0 -0
  95. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/resume.py +0 -0
  96. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/router.py +0 -0
  97. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/scheduler.py +0 -0
  98. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/self_report.py +0 -0
  99. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/self_report_local.py +0 -0
  100. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/sse.py +0 -0
  101. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/task_file.py +0 -0
  102. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/task_types.py +0 -0
  103. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/test.py +0 -0
  104. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/test_pipeline.py +0 -0
  105. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ticket_fields.py +0 -0
  106. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/tiers/__init__.py +0 -0
  107. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/tiers/mini_agent.py +0 -0
  108. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/__init__.py +0 -0
  109. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/dashboard.py +0 -0
  110. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/health.py +0 -0
  111. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/jobs.py +0 -0
  112. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/runner.py +0 -0
  113. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/security.py +0 -0
  114. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/service.py +0 -0
  115. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/session.py +0 -0
  116. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/setup_manager.py +0 -0
  117. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/agents.html +0 -0
  118. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/app.js +0 -0
  119. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/chat.html +0 -0
  120. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/custom-agents.html +0 -0
  121. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/default-agents.html +0 -0
  122. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/index.html +0 -0
  123. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/report.html +0 -0
  124. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/runs.html +0 -0
  125. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/schedules.html +0 -0
  126. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/setup.html +0 -0
  127. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/styles.css +0 -0
  128. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/ui/static/tickets.html +0 -0
  129. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/updater.py +0 -0
  130. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/url_access.py +0 -0
  131. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/sdk/write_hook.py +0 -0
  132. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/setup.cfg +0 -0
  133. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_activity.py +0 -0
  134. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_activity_tracking.py +0 -0
  135. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_chat_features.py +0 -0
  136. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_listing.py +0 -0
  137. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_prompt_async.py +0 -0
  138. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_prompt_models.py +0 -0
  139. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_run_ticket.py +0 -0
  140. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_schedules.py +0 -0
  141. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agent_workspace_settings.py +0 -0
  142. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_agents_connectors_builder.py +0 -0
  143. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_allowed_tools_config.py +0 -0
  144. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_auto_agent_dispatch.py +0 -0
  145. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_browser_profile.py +0 -0
  146. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_cheap_tier.py +0 -0
  147. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_client_jira_projects.py +0 -0
  148. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_config_token_diagnostics.py +0 -0
  149. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_configure_token_owner.py +0 -0
  150. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_console_encoding.py +0 -0
  151. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_departments.py +0 -0
  152. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_epic_decompose_run.py +0 -0
  153. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_epic_inherited_agent_id.py +0 -0
  154. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_epic_session_isolation.py +0 -0
  155. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_estimator.py +0 -0
  156. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_formats.py +0 -0
  157. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_git_action.py +0 -0
  158. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_graph_recon.py +0 -0
  159. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_graphify_build.py +0 -0
  160. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_graphify_hook_install.py +0 -0
  161. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_health.py +0 -0
  162. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_intake_plan_review.py +0 -0
  163. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_journal.py +0 -0
  164. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_local_exec_provider.py +0 -0
  165. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_phase_timer.py +0 -0
  166. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_plan_new_command.py +0 -0
  167. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_plan_run_activity.py +0 -0
  168. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_prd_artifact.py +0 -0
  169. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_proof_readers.py +0 -0
  170. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_providers.py +0 -0
  171. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_recon_session_isolation.py +0 -0
  172. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_resume.py +0 -0
  173. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_router.py +0 -0
  174. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_run_auto_agent_only.py +0 -0
  175. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_run_outputs.py +0 -0
  176. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_schedule_api.py +0 -0
  177. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_scheduler.py +0 -0
  178. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_self_report.py +0 -0
  179. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_service_token_warning.py +0 -0
  180. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_task_file_and_plan_run.py +0 -0
  181. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_task_file_format_doc.py +0 -0
  182. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_ticket_fields.py +0 -0
  183. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_ticket_run_resolve_by_id.py +0 -0
  184. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_ticket_speed_fixes.py +0 -0
  185. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_tier_flag_parity.py +0 -0
  186. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_update_timer.py +0 -0
  187. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_updater_extras.py +0 -0
  188. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_url_access.py +0 -0
  189. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_vps_health.py +0 -0
  190. {devops_bot_sdk-1.6.34 → devops_bot_sdk-1.6.37}/tests/test_write_hook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.34
3
+ Version: 1.6.37
4
4
  Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
5
5
  Author: noumanaziz2128
6
6
  License-Expression: LicenseRef-Proprietary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.6.34
3
+ Version: 1.6.37
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
@@ -16,6 +16,7 @@ sdk/crucial.py
16
16
  sdk/departments.py
17
17
  sdk/estimator.py
18
18
  sdk/exceptions.py
19
+ sdk/firewall.py
19
20
  sdk/formats.py
20
21
  sdk/git_ops.py
21
22
  sdk/github_ingest.py
@@ -38,6 +39,7 @@ sdk/scheduler.py
38
39
  sdk/self_report.py
39
40
  sdk/self_report_local.py
40
41
  sdk/sse.py
42
+ sdk/swagger.py
41
43
  sdk/task_file.py
42
44
  sdk/task_types.py
43
45
  sdk/test.py
@@ -112,7 +114,6 @@ sdk/ui/static/chat.html
112
114
  sdk/ui/static/custom-agents.html
113
115
  sdk/ui/static/default-agents.html
114
116
  sdk/ui/static/index.html
115
- sdk/ui/static/login.html
116
117
  sdk/ui/static/report.html
117
118
  sdk/ui/static/runs.html
118
119
  sdk/ui/static/schedules.html
@@ -152,7 +153,6 @@ tests/test_health.py
152
153
  tests/test_intake_plan_review.py
153
154
  tests/test_journal.py
154
155
  tests/test_local_exec_provider.py
155
- tests/test_otp_login.py
156
156
  tests/test_phase_timer.py
157
157
  tests/test_plan_new_command.py
158
158
  tests/test_plan_run_activity.py
@@ -168,12 +168,15 @@ tests/test_schedule_api.py
168
168
  tests/test_scheduler.py
169
169
  tests/test_self_report.py
170
170
  tests/test_service_token_warning.py
171
+ tests/test_swagger_command.py
171
172
  tests/test_task_file_and_plan_run.py
172
173
  tests/test_task_file_format_doc.py
173
174
  tests/test_ticket_fields.py
174
175
  tests/test_ticket_run_resolve_by_id.py
175
176
  tests/test_ticket_speed_fixes.py
176
177
  tests/test_tier_flag_parity.py
178
+ tests/test_ui_gate.py
179
+ tests/test_ui_lifespan.py
177
180
  tests/test_update_timer.py
178
181
  tests/test_updater_extras.py
179
182
  tests/test_url_access.py
@@ -2,4 +2,5 @@
2
2
  agentos = sdk.cli:main
3
3
  agentos-agentd = sdk.agentd:main
4
4
  agentos-auto = sdk.run_auto:main
5
+ agentos-swagger = sdk.swagger:main
5
6
  agentos-ui = sdk.ui.server:main
@@ -54,6 +54,7 @@ agentos = "sdk.cli:main" # unified CLI: configure/agent/integ
54
54
  agentos-auto = "sdk.run_auto:main" # run orchestrate-auto LOCALLY (co_ token from config)
55
55
  agentos-agentd = "sdk.agentd:main" # long-running custom-agent daemon
56
56
  agentos-ui = "sdk.ui.server:main" # local web control panel (needs the [ui] extra)
57
+ agentos-swagger = "sdk.swagger:main" # where /docs is + can you reach it (= `agentos swagger`)
57
58
 
58
59
  [project.urls]
59
60
  Homepage = "https://agentos.io"
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.6.34
3
+ Version: 1.6.37
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -34,7 +34,7 @@ Rules:
34
34
  - All data egress through submit_webhook only
35
35
  """
36
36
 
37
- __version__ = "1.6.34" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
37
+ __version__ = "1.6.37" # 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
@@ -58,6 +58,26 @@ Command surface:
58
58
  agentos integration rm <name>
59
59
  Remove a registered integration.
60
60
 
61
+ agentos firewall [--apply] [--revoke IP] [--port N]
62
+ Print the host-firewall rules that open the agentos-ui port to every IP in
63
+ the application allowlist (`sdk.config.get_allowed_ips`), for THIS OS — ufw
64
+ on Linux, `netsh advfirewall` on Windows, a pf note on macOS. Prints by
65
+ default; `--apply` executes them and needs root / Administrator. The host
66
+ firewall is a separate layer the allowlist cannot reach, and the two drifting
67
+ apart is what silently blinded every VPS when the backend's egress IP moved.
68
+ See sdk.firewall.
69
+
70
+ agentos swagger [--open] [--json [PATH]] [--port N]
71
+ Where the panel's Swagger UI is and whether it can actually be reached:
72
+ the URL derived from the bind address the service was installed with,
73
+ whether a co_ token is configured (masked), how many operations the spec
74
+ has, and a ufw/netsh diff against sdk.config.get_allowed_ips when the HOST
75
+ firewall would drop the port — a layer the SDK's own allowlist cannot see.
76
+ `--open` launches a browser; `--json` writes openapi.json from
77
+ create_app().openapi() with no server running (`--json -` pipes it).
78
+ Also flags routes registered twice, since the second copy is dead code.
79
+ Needs the [ui] extra for the route count and --json. See sdk.swagger.
80
+
61
81
  agentos report submit [--date YYYY-MM-DD] (--text "…" | --file PATH)
62
82
  [--project NAME] [--proof PATH|URL]… [--no-wait]
63
83
  Report work that never reached Jira or GitHub, attach proofs (local
@@ -139,6 +159,14 @@ def main() -> None:
139
159
  _github_command(rest)
140
160
  return
141
161
 
162
+ if noun == "firewall":
163
+ from sdk import firewall
164
+ sys.exit(firewall.cli(rest))
165
+
166
+ if noun == "swagger":
167
+ from sdk import swagger
168
+ sys.exit(swagger.cli(rest))
169
+
142
170
  if noun == "report":
143
171
  _report_command(rest)
144
172
  return
@@ -152,7 +180,7 @@ def main() -> None:
152
180
 
153
181
 
154
182
  def _print_usage() -> None:
155
- print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report|browser> ...")
183
+ print("usage: agentos <configure|chat|agent|integration|default-agents|agentd|github|report|browser|firewall|swagger> ...")
156
184
  print()
157
185
  print(" agentos configure [--rotate]")
158
186
  print(" agentos chat [--product-id ID] [--pv-id ID] [--no-autorun] # requirements → ticket")
@@ -164,6 +192,8 @@ def _print_usage() -> None:
164
192
  print(" agentos github <ingest [--days N]|status> # PR ingestion; --days N backfills N days")
165
193
  print(" agentos report <submit|list|show ID> # report work Jira/GitHub never saw")
166
194
  print(" agentos browser <login URL|status|logout> # sign in once; private links become readable")
195
+ print(" agentos firewall [--apply] [--revoke IP] [--port N] # open the UI port to the allowlisted IPs")
196
+ print(" agentos swagger [--open] [--json [PATH]] # where /docs is, and can you reach it")
167
197
 
168
198
 
169
199
  # ── agent ────────────────────────────────────────────────────────────────
@@ -45,6 +45,13 @@ logger = logging.getLogger(__name__)
45
45
  # agents without a lookup on every run.
46
46
  _AGENT_ID_CACHE: dict[str, str] = {}
47
47
 
48
+ # GitHub access token, keyed by the co_ token that fetched it → (token, expires_at
49
+ # monotonic). Keyed per credential so two clients on one box never share a token.
50
+ # Without this the token is re-fetched on EVERY call, which is what
51
+ # tests/test_ticket_speed_fixes.py::TestGithubTokenCache guards against.
52
+ _GITHUB_TOKEN_CACHE: dict[str, tuple[str, float]] = {}
53
+ _GITHUB_TOKEN_TTL_S = float(os.getenv("AGENTOS_GITHUB_TOKEN_TTL_S", "") or 1800.0)
54
+
48
55
  # GitHub access token, keyed by the co_ token that fetched it → (token, expires_at
49
56
  # monotonic). Keyed per credential so two clients on one box never share a token.
50
57
  _GITHUB_TOKEN_CACHE: dict[str, tuple[str, float]] = {}
@@ -952,51 +959,71 @@ class BackendClient:
952
959
  co_ token. Best-effort: returns None on any failure so a run without a
953
960
  connected GitHub account still proceeds (clone/PR steps just won't auth).
954
961
  """
962
+ import time as _time
963
+
964
+ now = _time.monotonic()
965
+ cached = _GITHUB_TOKEN_CACHE.get(self._token)
966
+ if cached and now < cached[1]:
967
+ return cached[0]
955
968
  record = await self.github_token_record()
956
- return (record or {}).get("accessToken") or None
969
+ token = (record or {}).get("accessToken") or None
970
+ if token:
971
+ _GITHUB_TOKEN_CACHE[self._token] = (token, now + _GITHUB_TOKEN_TTL_S)
972
+ return token
957
973
 
958
- async def ingest_github_prs(self, since: str | None = None) -> dict:
974
+ async def ingest_github_prs(self, since: str | None = None) -> dict | None:
959
975
  """POST {AI}/api/v1/github/prs — trigger this user's daily PR ingestion.
960
976
 
961
- The backend resolves the caller's GitHub link server-side from the co_
962
- token in ``Authorization`` (see ``docs/GITHUB_SCHEDULING_SDK_MIGRATION.md``);
963
- ``githubUsername``/``githubAccessToken`` below are placeholders kept only
964
- for backends that predate that resolution and are otherwise ignored.
965
- ``since`` (ISO-8601 date) scopes the fetch — omit for "today".
966
-
967
- Returns the backend's ``{status, job_id, status_url, ...}`` envelope.
968
- Raises BackendUnreachable / HTTP errors — the github_ingest cron catches
969
- and logs these itself rather than treating them as success.
977
+ The daily trigger. The work runs on the AI backend (LLM summarise, the
978
+ downstream daily-summary forward, the Qdrant upsert); the SDK only decides
979
+ WHEN, because only this machine knows its own catch-up watermark.
980
+
981
+ The backend resolves the caller's GitHub link server-side from the co_ token
982
+ in ``Authorization`` and OVERWRITES whatever the body sends
983
+ (``docs/GITHUB_SCHEDULING_SDK_MIGRATION.md`` §7/§9 — that endpoint used to
984
+ take a GitHub access token in an unauthenticated body). So this deliberately
985
+ sends NO credentials: a client that still supplied them would be shipping a
986
+ secret the server ignores.
987
+
988
+ ``since`` is an ISO date (``YYYY-MM-DD``) → the backend's ``date_from``.
989
+ Omitted means "today", which is what a never-missed run wants; the cron sends
990
+ the watermark instead so a machine that was off does not lose days.
991
+
992
+ Returns the response JSON (``{status, job_id, status_url, ...}``) or **None**
993
+ on any failure — never raises into the cron. WHY it failed lands in
994
+ ``self.last_ingest_error`` (``{status, body, retryable}``), which the
995
+ scheduler needs to tell a busy backend (retry in 30 min) from a rejected
996
+ request (wait for the next slot).
970
997
  """
971
- body = [{
998
+ payload = [{
972
999
  "userId": self._user_id_from_token(),
973
- "githubUsername": "",
974
- "githubAccessToken": "",
975
- "apiTokenHash": self._token,
976
- "date_from": since,
1000
+ "apiTokenHash": self._token, # downstream daily-summary Authorization
977
1001
  }]
978
- async with httpx.AsyncClient(timeout=30.0) as client:
979
- resp = await client.post(
980
- self._url("/api/v1/github/prs"), json=body, headers=self._headers,
981
- )
982
- _check_status(resp.status_code, self._url("/api/v1/github/prs"))
983
- return resp.json()
984
-
985
- async def report_vps_health(self, payload: dict) -> dict:
986
- """PATCH {AI}/api/v1/vps/health — heartbeat this machine's resource usage.
987
-
988
- Feeds the backend's ``contabo_allocation`` row (VPS health shown in chat /
989
- activity reports) — a live GET of the same path answers on-demand checks,
990
- this PATCH is what keeps that data from going stale. Raises on failure
991
- (BackendUnreachable / HTTP error); the vps_health cron logs and continues
992
- rather than crashing the loop.
993
- """
994
- async with httpx.AsyncClient(timeout=30.0) as client:
995
- resp = await client.request(
996
- "PATCH", self._url("/api/v1/vps/health"), json=payload, headers=self._headers,
997
- )
998
- _check_status(resp.status_code, self._url("/api/v1/vps/health"))
999
- return resp.json()
1002
+ if since:
1003
+ payload[0]["date_from"] = since
1004
+ self.last_ingest_error = {}
1005
+ try:
1006
+ async with httpx.AsyncClient(timeout=60.0) as client:
1007
+ resp = await client.post(
1008
+ self._url("/api/v1/github/prs"), json=payload, headers=self._headers,
1009
+ )
1010
+ if resp.status_code >= 400:
1011
+ body = resp.text[:500]
1012
+ logger.warning("client.ingest_github_prs failed status=%s body=%s",
1013
+ resp.status_code, body[:200])
1014
+ self.last_ingest_error = {
1015
+ "status": resp.status_code,
1016
+ "body": body,
1017
+ "retryable": is_retryable_backend_error(resp.status_code, body),
1018
+ }
1019
+ return None
1020
+ return resp.json()
1021
+ except Exception as exc: # noqa: BLE001 — a cron must not die on a blip
1022
+ logger.warning("client.ingest_github_prs error err=%s", exc)
1023
+ # A transport failure (timeout, reset, DNS blip) is as retryable as an
1024
+ # exhausted pool — the backend may simply be busy.
1025
+ self.last_ingest_error = {"status": 0, "body": str(exc)[:500], "retryable": True}
1026
+ return None
1000
1027
 
1001
1028
  async def move_to_backlog(self, task_id: str) -> bool:
1002
1029
  """PUT {BE}/tasks-ml/{id} status=backlog (best-effort).
@@ -192,7 +192,15 @@ def _dotenv_value(key: str) -> str:
192
192
  # backend can reach EVERY VPS's /api/ui/* out of the box (no per-VPS .env needed);
193
193
  # always allowed, treated as permanent (not removable via the API). Extend/override
194
194
  # with AGENTOS_UI_ALLOWED_IPS in .env or the process env.
195
- _DEFAULT_ALLOWED_IPS: list[str] = ["20.12.201.148"]
195
+ #
196
+ # A LIST, not one value, because the backend's egress address is not stable: when it
197
+ # moved to 20.127.87.169 every VPS silently stopped answering `GET /api/v1/vps/health`
198
+ # (30s hang, then 502 — the SYNs were dropped at the host firewall, observed
199
+ # 2026-09-03). Both addresses stay listed rather than swapping, so a rollback or a
200
+ # blue/green pair does not re-break it. Note the host firewall is a SEPARATE layer this
201
+ # list cannot reach: `sudo ufw allow from <ip> to any port 8765 proto tcp` is still
202
+ # required per VPS. See docs/VPS_HEALTH_AND_GITHUB_INGEST_DIAGNOSIS.md #5.
203
+ _DEFAULT_ALLOWED_IPS: list[str] = ["20.12.201.148", "20.127.87.169"]
196
204
 
197
205
 
198
206
  def _parse_token_list(raw: str) -> list[str]:
@@ -0,0 +1,310 @@
1
+ """Host-firewall rules for the ``agentos-ui`` port — generate, show, apply.
2
+
3
+ The SDK has **two** independent gates in front of `/api/ui/*`, and only one of
4
+ them is code:
5
+
6
+ 1. `sdk.config.get_allowed_ips()` + `IPAllowlistMiddleware` — application level,
7
+ shipped in the wheel, changed by editing `.env` / `config.toml`.
8
+ 2. The **host firewall** — OS level, outside the process entirely.
9
+
10
+ Nothing in the SDK could reach (2), which is how the backend's egress move to
11
+ `20.127.87.169` silently blinded every VPS: the allowlist said yes, the kernel
12
+ dropped the SYN before the server ever saw it (`docs/VPS_HEALTH_AND_GITHUB_INGEST
13
+ _DIAGNOSIS.md` #5). This module closes that gap by deriving the firewall rules
14
+ from the *same* list the middleware enforces, so the two layers cannot drift.
15
+
16
+ agentos firewall # show the rules for this OS (safe, prints only)
17
+ agentos firewall --apply # run them (needs root / Administrator)
18
+ agentos firewall --revoke IP # print the matching delete rule
19
+
20
+ Applying is deliberately opt-in: editing a live firewall is exactly the operation
21
+ that can lock an operator out of their own box, so the default is to print and let
22
+ a human read it first.
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import os
27
+ import platform
28
+ import shutil
29
+ import subprocess
30
+ import sys
31
+
32
+ _RULE_TAG = "AgentOS UI" # stable prefix so rules are findable and removable
33
+
34
+
35
+ def ui_port() -> int:
36
+ """The port `agentos-ui` binds — same resolution the server uses."""
37
+ try:
38
+ return int(os.environ.get("AGENTOS_UI_PORT", "8765"))
39
+ except ValueError:
40
+ return 8765
41
+
42
+
43
+ def _system() -> str:
44
+ return platform.system().lower()
45
+
46
+
47
+ def allow_rules(ips: list[str], port: int, system: str | None = None) -> list[str]:
48
+ """The commands that open ``port`` to each of ``ips`` — one per IP.
49
+
50
+ Linux uses ufw. Windows uses `netsh advfirewall`, whose rule model differs in
51
+ two ways worth naming: the rule is identified by `name=`, not by its contents,
52
+ so a stable name is what makes it idempotent-ish and removable; and `remoteip=`
53
+ accepts a comma-separated list, so all the IPs *could* be one rule — kept as
54
+ one-rule-per-IP anyway so a single address can be revoked without rewriting
55
+ the others.
56
+ """
57
+ sysname = (system or _system()).lower()
58
+ out: list[str] = []
59
+
60
+ if sysname == "linux":
61
+ for ip in ips:
62
+ # `comment` must come last; ufw >= 0.35. Harmless to re-run — ufw
63
+ # reports "Skipping adding existing rule".
64
+ out.append(
65
+ f"sudo ufw allow from {ip} to any port {port} proto tcp "
66
+ f"comment '{_RULE_TAG}'"
67
+ )
68
+ elif sysname == "windows":
69
+ for ip in ips:
70
+ out.append(
71
+ f'netsh advfirewall firewall add rule name="{_RULE_TAG} {port} {ip}" '
72
+ f"dir=in action=allow protocol=TCP localport={port} remoteip={ip}"
73
+ )
74
+ elif sysname == "darwin":
75
+ # macOS ships pf, which has no per-rule CLI equivalent — rules live in a
76
+ # file and the anchor is reloaded. Emitted as a here-doc + reload rather
77
+ # than pretending a one-liner exists.
78
+ joined = ", ".join(ips) or "none"
79
+ out.append(
80
+ f"# macOS (pf) — append to /etc/pf.anchors/agentos then reload:\n"
81
+ f"# pass in proto tcp from {{ {joined} }} to any port {port}\n"
82
+ f"sudo pfctl -f /etc/pf.conf && sudo pfctl -E"
83
+ )
84
+ else:
85
+ out.append(f"# unsupported platform {sysname!r} — open TCP {port} to: "
86
+ f"{', '.join(ips) or 'none'}")
87
+ return out
88
+
89
+
90
+ def revoke_rule(ip: str, port: int, system: str | None = None) -> str:
91
+ """The command that removes the rule for one ``ip``."""
92
+ sysname = (system or _system()).lower()
93
+ if sysname == "linux":
94
+ return f"sudo ufw delete allow from {ip} to any port {port} proto tcp"
95
+ if sysname == "windows":
96
+ return (f'netsh advfirewall firewall delete rule '
97
+ f'name="{_RULE_TAG} {port} {ip}" remoteip={ip}')
98
+ if sysname == "darwin":
99
+ return f"# macOS: remove the matching line from /etc/pf.anchors/agentos, then: sudo pfctl -f /etc/pf.conf"
100
+ return f"# unsupported platform {sysname!r}"
101
+
102
+
103
+ def _tool_present() -> tuple[bool, str]:
104
+ """Is the firewall tool for this OS actually installed?"""
105
+ sysname = _system()
106
+ tool = {"linux": "ufw", "windows": "netsh", "darwin": "pfctl"}.get(sysname, "")
107
+ if not tool:
108
+ return False, f"no known firewall tool for {sysname}"
109
+ if shutil.which(tool):
110
+ return True, tool
111
+ return False, f"{tool} not found on PATH"
112
+
113
+
114
+ def _run(cmd: list[str], timeout: float = 10.0) -> str | None:
115
+ """Run a read-only query and return stdout, or None if it could not be run."""
116
+ try:
117
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
118
+ except Exception: # noqa: BLE001 — tool missing, no permission, hung
119
+ return None
120
+ return proc.stdout if proc.returncode == 0 else None
121
+
122
+
123
+ def _parse_ufw(text: str, port: int) -> set[str]:
124
+ """Sources ufw currently allows to ``port`` (IPv4 rules only).
125
+
126
+ Parses the human table, because `ufw status` has no machine format. A row is
127
+ ``<to> <action> <from>`` and the interesting ones look like::
128
+
129
+ 8765/tcp ALLOW 20.12.201.148 # app port - requested IP
130
+ 8765/tcp ALLOW Anywhere
131
+
132
+ ``(v6)`` rows are skipped so the result can be diffed against an IPv4
133
+ allowlist without spurious "missing" entries.
134
+ """
135
+ out: set[str] = set()
136
+ for line in text.splitlines():
137
+ line = line.split("#", 1)[0].strip()
138
+ if not line or "(v6)" in line:
139
+ continue
140
+ parts = line.split()
141
+ if len(parts) < 3 or parts[1].upper() != "ALLOW":
142
+ continue
143
+ to = parts[0]
144
+ if to != str(port) and not to.startswith(f"{port}/"):
145
+ continue
146
+ out.add(" ".join(parts[2:]).strip())
147
+ return out
148
+
149
+
150
+ def _parse_netsh(text: str, port: int) -> set[str]:
151
+ """Sources netsh currently allows to ``port`` (inbound allow rules only).
152
+
153
+ `show rule` emits blank-line-separated blocks of ``Key: Value``; a rule matters
154
+ only when it is inbound, allowing, and its LocalPort covers ``port``.
155
+ """
156
+ out: set[str] = set()
157
+ for block in text.split("\n\n"):
158
+ fields: dict[str, str] = {}
159
+ for line in block.splitlines():
160
+ if ":" in line:
161
+ k, _, v = line.partition(":")
162
+ fields[k.strip().lower()] = v.strip()
163
+ if not fields:
164
+ continue
165
+ if fields.get("enabled", "").lower() not in ("yes", ""):
166
+ continue
167
+ if fields.get("direction", "").lower() not in ("in", "inbound", ""):
168
+ continue
169
+ if fields.get("action", "").lower() not in ("allow", ""):
170
+ continue
171
+ ports = {p.strip() for p in fields.get("localport", "").split(",")}
172
+ if str(port) not in ports and "any" not in {p.lower() for p in ports}:
173
+ continue
174
+ for ip in fields.get("remoteip", "").split(","):
175
+ ip = ip.strip()
176
+ if ip:
177
+ out.add("Anywhere" if ip.lower() in ("any", "*") else ip)
178
+ return out
179
+
180
+
181
+ def port_sources(port: int, system: str | None = None) -> set[str] | None:
182
+ """Which sources the HOST firewall currently allows to ``port``.
183
+
184
+ ``{"Anywhere"}`` means open to the world. **``None`` means the state could not
185
+ be read** — no tool, or not privileged — and a caller must then say "unknown",
186
+ never "closed": reporting a closed port that is actually open is the more
187
+ dangerous error of the two.
188
+
189
+ Read-only and never raises. This exists because the SDK's own allowlist
190
+ (`sdk.config.get_allowed_ips`) and the host firewall are separate layers, and
191
+ the interesting question is where they DISAGREE — an IP the app permits but the
192
+ kernel drops looks exactly like a broken backend.
193
+ """
194
+ sysname = (system or _system()).lower()
195
+ if sysname == "linux":
196
+ text = _run(["ufw", "status"])
197
+ if text is None:
198
+ return None
199
+ got = _parse_ufw(text, port)
200
+ return {"Anywhere"} if "Anywhere" in got else got
201
+ if sysname == "windows":
202
+ text = _run(["netsh", "advfirewall", "firewall", "show", "rule",
203
+ "name=all", "dir=in"], timeout=25.0)
204
+ if text is None:
205
+ return None
206
+ got = _parse_netsh(text, port)
207
+ return {"Anywhere"} if "Anywhere" in got else got
208
+ return None # macOS pf and anything else: not readable this way
209
+
210
+
211
+ def _is_privileged() -> bool:
212
+ if _system() == "windows":
213
+ try:
214
+ import ctypes
215
+ return bool(ctypes.windll.shell32.IsUserAnAdmin())
216
+ except Exception: # noqa: BLE001 — not Windows, or no ctypes
217
+ return False
218
+ return hasattr(os, "geteuid") and os.geteuid() == 0
219
+
220
+
221
+ def cli(argv: list[str]) -> int:
222
+ """``agentos firewall [--apply] [--revoke IP] [--port N]``."""
223
+ from sdk.config import get_allowed_ips
224
+
225
+ port = ui_port()
226
+ if "--port" in argv:
227
+ try:
228
+ port = int(argv[argv.index("--port") + 1])
229
+ except (IndexError, ValueError):
230
+ print("error: --port needs a number")
231
+ return 2
232
+
233
+ if "--revoke" in argv:
234
+ try:
235
+ ip = argv[argv.index("--revoke") + 1]
236
+ except IndexError:
237
+ print("error: --revoke needs an IP")
238
+ return 2
239
+ print(revoke_rule(ip, port))
240
+ print("\nAlso remove it from the allowlist, or the next restart re-permits it\n"
241
+ "at the application layer: AGENTOS_UI_ALLOWED_IPS in ~/.agentos/.env,\n"
242
+ "or `allowed_ips` in ~/.agentos/config.toml. IPs baked into\n"
243
+ "sdk.config._DEFAULT_ALLOWED_IPS cannot be removed that way.")
244
+ return 0
245
+
246
+ ips = get_allowed_ips()
247
+ sysname = _system()
248
+ print(f"Platform : {platform.system()} ({sysname})")
249
+ print(f"UI port : {port}")
250
+ print(f"Allowed : {', '.join(ips) if ips else '(none — loopback only)'}")
251
+
252
+ if not ips:
253
+ print("\nNothing to open. Loopback always reaches the server; add the backend's\n"
254
+ "egress IP to AGENTOS_UI_ALLOWED_IPS first.")
255
+ return 0
256
+
257
+ rules = allow_rules(ips, port, sysname)
258
+ print("\nFirewall rules for this host:\n")
259
+ for r in rules:
260
+ print(f" {r}")
261
+
262
+ if "--apply" not in argv:
263
+ ok, tool = _tool_present()
264
+ print(f"\n(dry run — nothing changed. Firewall tool: {tool})")
265
+ print("Re-run with --apply to execute, or copy the lines above.")
266
+ if sysname == "linux":
267
+ print("Verify after applying: sudo ufw status")
268
+ elif sysname == "windows":
269
+ print(f'Verify after applying: netsh advfirewall firewall show rule name="{_RULE_TAG} {port} {ips[0]}"')
270
+ return 0
271
+
272
+ if sysname == "darwin":
273
+ print("\n--apply is not supported on macOS: pf rules live in a file, so applying\n"
274
+ "them means editing /etc/pf.anchors/agentos by hand. Follow the note above.")
275
+ return 1
276
+
277
+ ok, tool = _tool_present()
278
+ if not ok:
279
+ print(f"\nerror: {tool}")
280
+ return 1
281
+ if not _is_privileged():
282
+ who = "Administrator" if sysname == "windows" else "root (sudo)"
283
+ print(f"\nerror: --apply needs {who}.")
284
+ return 1
285
+
286
+ failed = 0
287
+ print()
288
+ for r in rules:
289
+ # The printed form carries `sudo` for humans; we are already privileged.
290
+ cmd = r[len("sudo "):] if r.startswith("sudo ") else r
291
+ try:
292
+ proc = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
293
+ except Exception as exc: # noqa: BLE001 — never leave the firewall half-known
294
+ print(f" ✗ {cmd}\n {exc}")
295
+ failed += 1
296
+ continue
297
+ tail = (proc.stdout or proc.stderr or "").strip().splitlines()
298
+ note = tail[-1][:100] if tail else ""
299
+ if proc.returncode == 0:
300
+ print(f" ✓ {cmd}\n {note}" if note else f" ✓ {cmd}")
301
+ else:
302
+ print(f" ✗ {cmd} (exit {proc.returncode})\n {note}")
303
+ failed += 1
304
+
305
+ print(f"\n{len(rules) - failed}/{len(rules)} applied.")
306
+ return 1 if failed else 0
307
+
308
+
309
+ if __name__ == "__main__": # pragma: no cover
310
+ sys.exit(cli(sys.argv[1:]))