devops-bot-sdk 1.4.141__tar.gz → 1.4.145__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 (95) hide show
  1. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/devops_bot_sdk.egg-info/SOURCES.txt +4 -1
  4. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/client.py +16 -0
  6. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/run_auto.py +8 -2
  7. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/api.py +32 -0
  8. devops_bot_sdk-1.4.145/sdk/ui/health.py +80 -0
  9. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/updater.py +24 -2
  10. devops_bot_sdk-1.4.145/tests/test_health.py +53 -0
  11. devops_bot_sdk-1.4.145/tests/test_updater_extras.py +49 -0
  12. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/README.md +0 -0
  13. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  14. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  15. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/devops_bot_sdk.egg-info/requires.txt +0 -0
  16. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  17. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/pyproject.toml +0 -0
  18. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agentd.py +0 -0
  19. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/__init__.py +0 -0
  20. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/builder.py +0 -0
  21. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/chat.py +0 -0
  22. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/connectors.py +0 -0
  23. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/engines/__init__.py +0 -0
  24. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/engines/claude_code.py +0 -0
  25. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/engines/http.py +0 -0
  26. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/engines/mcp.py +0 -0
  27. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/engines/n8n.py +0 -0
  28. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/engines/shell.py +0 -0
  29. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/integrations.py +0 -0
  30. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/inventory.py +0 -0
  31. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/library/__init__.py +0 -0
  32. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/manifest.py +0 -0
  33. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/prompt_studio.py +0 -0
  34. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/prompt_template.py +0 -0
  35. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/prompt_vars.py +0 -0
  36. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/run_log.py +0 -0
  37. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/secrets.py +0 -0
  38. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/usage.py +0 -0
  39. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/agents/varstore.py +0 -0
  40. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/cli.py +0 -0
  41. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/collectors/__init__.py +0 -0
  42. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/collectors/files.py +0 -0
  43. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/collectors/process.py +0 -0
  44. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/collectors/screenshot.py +0 -0
  45. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/config.py +0 -0
  46. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/crucial.py +0 -0
  47. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/design_verify/__init__.py +0 -0
  48. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/design_verify/assets.py +0 -0
  49. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/design_verify/bootstrap.py +0 -0
  50. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/design_verify/browser.py +0 -0
  51. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/design_verify/compare.py +0 -0
  52. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/design_verify/loop.py +0 -0
  53. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/exceptions.py +0 -0
  54. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/git_ops.py +0 -0
  55. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/graphify.py +0 -0
  56. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/hooks/__init__.py +0 -0
  57. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/hooks/crucial_guard.py +0 -0
  58. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ipc/__init__.py +0 -0
  59. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ipc/electron_bridge.py +0 -0
  60. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ipc/handlers.py +0 -0
  61. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/local_exec.py +0 -0
  62. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/models/__init__.py +0 -0
  63. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/models/envelope.py +0 -0
  64. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/models/requests.py +0 -0
  65. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/models/responses.py +0 -0
  66. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/models/snapshots.py +0 -0
  67. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/platform_compat.py +0 -0
  68. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/py.typed +0 -0
  69. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/sse.py +0 -0
  70. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/test.py +0 -0
  71. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/test_pipeline.py +0 -0
  72. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/__init__.py +0 -0
  73. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/dashboard.py +0 -0
  74. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/ip_allowlist.py +0 -0
  75. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/jobs.py +0 -0
  76. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/models.py +0 -0
  77. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/runner.py +0 -0
  78. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/security.py +0 -0
  79. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/server.py +0 -0
  80. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/service.py +0 -0
  81. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/setup_manager.py +0 -0
  82. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/static/agents.html +0 -0
  83. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/static/app.js +0 -0
  84. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/static/custom-agents.html +0 -0
  85. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/static/default-agents.html +0 -0
  86. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/static/index.html +0 -0
  87. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/static/runs.html +0 -0
  88. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/static/setup.html +0 -0
  89. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/static/styles.css +0 -0
  90. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/sdk/ui/static/tickets.html +0 -0
  91. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/setup.cfg +0 -0
  92. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/tests/test_agent_chat_features.py +0 -0
  93. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/tests/test_agent_prompt_models.py +0 -0
  94. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/tests/test_agents_connectors_builder.py +0 -0
  95. {devops_bot_sdk-1.4.141 → devops_bot_sdk-1.4.145}/tests/test_git_action.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.141
3
+ Version: 1.4.145
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.4.141
3
+ Version: 1.4.145
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
@@ -67,6 +67,7 @@ sdk/models/snapshots.py
67
67
  sdk/ui/__init__.py
68
68
  sdk/ui/api.py
69
69
  sdk/ui/dashboard.py
70
+ sdk/ui/health.py
70
71
  sdk/ui/ip_allowlist.py
71
72
  sdk/ui/jobs.py
72
73
  sdk/ui/models.py
@@ -87,4 +88,6 @@ sdk/ui/static/tickets.html
87
88
  tests/test_agent_chat_features.py
88
89
  tests/test_agent_prompt_models.py
89
90
  tests/test_agents_connectors_builder.py
90
- tests/test_git_action.py
91
+ tests/test_git_action.py
92
+ tests/test_health.py
93
+ tests/test_updater_extras.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.141
3
+ Version: 1.4.145
4
4
 
5
5
  Public surface:
6
6
  BackendClient.from_config() — create client from ~/.agentos/config.toml
@@ -30,7 +30,7 @@ Rules:
30
30
  - All data egress through submit_webhook only
31
31
  """
32
32
 
33
- __version__ = "1.4.141" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
33
+ __version__ = "1.4.145" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
34
34
  __author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
35
35
 
36
36
  from sdk.client import BackendClient
@@ -1227,6 +1227,22 @@ class BackendClient:
1227
1227
  _check_status(resp.status_code, self._base_url)
1228
1228
  return resp.json().get("agents", [])
1229
1229
 
1230
+ async def list_product_agents(self, product_id: str) -> dict:
1231
+ """GET /api/v1/product/agents?product_id=… — a product's custom agents.
1232
+
1233
+ The backend resolves the product's VPS IP from ``product_id`` and proxies
1234
+ to the product's ``/orchestrator/agents`` endpoint, returning the purified
1235
+ ``{response, product_id}`` shape. Used by the product-mode agents page.
1236
+ """
1237
+ async with httpx.AsyncClient(timeout=30.0) as client:
1238
+ resp = await client.get(
1239
+ self._url("/api/v1/product/agents"),
1240
+ params={"product_id": product_id},
1241
+ headers=self._headers,
1242
+ )
1243
+ _check_status(resp.status_code, self._base_url)
1244
+ return resp.json()
1245
+
1230
1246
  async def get_custom_agents(self) -> list[CustomAgent]:
1231
1247
  """GET /api/v1/agents → parsed :class:`CustomAgent` rows.
1232
1248
 
@@ -572,10 +572,16 @@ def main() -> None:
572
572
  try:
573
573
  from sdk.ui import service as ui_service
574
574
  ok, msg = ui_service.restart()
575
+ if not ok:
576
+ # No managed service yet (panel was started by hand) — install one
577
+ # so the panel comes up now AND after every reboot/update, on Linux
578
+ # (systemd), Windows (Scheduled Task) and macOS (launchd). This is
579
+ # what removes the manual "install + up agentos-ui" step.
580
+ ok, msg = ui_service.install()
575
581
  print(f" control panel: {msg}" if ok
576
- else f" control panel: not reloaded — {msg}")
582
+ else f" control panel: not started — {msg}")
577
583
  except Exception as exc: # noqa: BLE001 — never fail the update on this
578
- print(f" control panel: not reloaded ({exc})")
584
+ print(f" control panel: not started ({exc})")
579
585
  pid = _daemon_running()
580
586
  if pid:
581
587
  print("note: a background loop is running the OLD version — "
@@ -94,6 +94,20 @@ async def status() -> dict:
94
94
  }
95
95
 
96
96
 
97
+ @router.get("/health", dependencies=[_auth])
98
+ async def health() -> dict:
99
+ """VPS health snapshot — live CPU/RAM/disk/uptime + auto-loop state + version.
100
+
101
+ The backend pings this (``GET /api/v1/vps/health`` → this endpoint) to report
102
+ on each VPS. Resource metrics need the ``[process]`` extra (psutil); without
103
+ it ``resources.available`` is False rather than an error. Token-gated + IP-
104
+ allowlisted like every other ``/api/ui/*`` route.
105
+ """
106
+ from sdk.ui.health import collect_health
107
+
108
+ return collect_health()
109
+
110
+
97
111
  # ── IP allowlist management ─────────────────────────────────────────────────
98
112
  # Whitelist a client IP for THIS SDK. A whitelisted IP passes the IP gate for
99
113
  # EVERY path (all /api/ui/* endpoints + the UI + /docs); data endpoints still
@@ -314,6 +328,24 @@ async def connectors() -> dict:
314
328
  }
315
329
 
316
330
 
331
+ @router.get("/product-agents", dependencies=[_auth])
332
+ async def product_agents(product_id: str = Query(...)) -> dict:
333
+ """A product's custom agents, for the product-mode agents page.
334
+
335
+ Given ``product_id``, the backend resolves the product's VPS IP (from
336
+ ``products_vps``) and proxies to the product's ``/orchestrator/agents``,
337
+ returning ``{response, product_id}`` (``response`` holds the agents). Product
338
+ mode is the either/or alternative to the custom+default page — never both.
339
+ Best-effort: on backend error returns ``{response: [], product_id, error}``.
340
+ """
341
+ from sdk.client import BackendClient
342
+
343
+ try:
344
+ return await BackendClient.from_config().list_product_agents(product_id)
345
+ except Exception as exc: # noqa: BLE001 — degrade to empty, surface the reason
346
+ return {"response": [], "product_id": product_id, "error": str(exc)}
347
+
348
+
317
349
  @router.get("/agents", dependencies=[_auth])
318
350
  async def agents() -> dict:
319
351
  """The caller's custom agents (``relation_type=person``) from the backend.
@@ -0,0 +1,80 @@
1
+ """VPS health snapshot for ``GET /api/ui/health``.
2
+
3
+ Gathers live host metrics (CPU / RAM / disk / uptime) plus the auto-loop state and
4
+ SDK version, so the backend can ping each VPS for reporting/monitoring. Resource
5
+ metrics use ``psutil`` when available (the ``[process]`` extra) and degrade
6
+ gracefully — ``resources.available=False`` — when it isn't, rather than erroring.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import platform
11
+ import socket
12
+ import time
13
+ from datetime import datetime, timezone
14
+
15
+
16
+ def _resources() -> dict:
17
+ """CPU/RAM/disk/uptime via psutil; ``{"available": False}`` when psutil absent."""
18
+ try:
19
+ import psutil
20
+ except Exception: # noqa: BLE001 — psutil optional ([process] extra)
21
+ return {"available": False, "reason": "psutil not installed (pip install 'devops-bot-sdk[process]')"}
22
+
23
+ out: dict = {"available": True}
24
+ try:
25
+ out["cpu_percent"] = round(float(psutil.cpu_percent(interval=0.1)), 2)
26
+ except Exception: # noqa: BLE001
27
+ out["cpu_percent"] = None
28
+ try:
29
+ vm = psutil.virtual_memory()
30
+ out["ram"] = {
31
+ "used_mb": round(vm.used / 1048576, 1),
32
+ "total_mb": round(vm.total / 1048576, 1),
33
+ "percent": round(float(vm.percent), 2),
34
+ }
35
+ except Exception: # noqa: BLE001
36
+ out["ram"] = None
37
+ try:
38
+ du = psutil.disk_usage("/")
39
+ out["disk"] = {
40
+ "used_gb": round(du.used / 1073741824, 2),
41
+ "total_gb": round(du.total / 1073741824, 2),
42
+ "percent": round(float(du.percent), 2),
43
+ }
44
+ except Exception: # noqa: BLE001
45
+ out["disk"] = None
46
+ try:
47
+ out["uptime_seconds"] = int(time.time() - psutil.boot_time())
48
+ except Exception: # noqa: BLE001
49
+ out["uptime_seconds"] = None
50
+ return out
51
+
52
+
53
+ def collect_health() -> dict:
54
+ """Full health snapshot: host, live resources, auto-loop state, SDK version."""
55
+ import sdk as _sdk
56
+
57
+ # Loop state — best-effort (never fail the health probe on it).
58
+ try:
59
+ from sdk.ui.runner import loop_state
60
+ loop = loop_state()
61
+ except Exception as exc: # noqa: BLE001
62
+ loop = {"error": str(exc)}
63
+
64
+ try:
65
+ hostname = socket.gethostname()
66
+ except Exception: # noqa: BLE001
67
+ hostname = ""
68
+
69
+ return {
70
+ "status": "ok",
71
+ "timestamp": datetime.now(timezone.utc).isoformat(),
72
+ "sdk_version": getattr(_sdk, "__version__", "0.0.0"),
73
+ "host": {
74
+ "hostname": hostname,
75
+ "os": platform.platform(),
76
+ "python": platform.python_version(),
77
+ },
78
+ "resources": _resources(),
79
+ "loop": loop,
80
+ }
@@ -20,11 +20,31 @@ import subprocess
20
20
  import sys
21
21
 
22
22
  _DIST = "devops-bot-sdk"
23
+ _EXTRAS = "[ui]" # control-panel deps (fastapi/uvicorn/starlette) — keep on every self-update
23
24
  _GITHUB_REPO = "consultancy-outfit/DevOpsBot-SDK"
24
25
  _GITHUB_SSH = f"git+ssh://git@github.com/{_GITHUB_REPO}.git"
25
26
  _PYPI_JSON = f"https://pypi.org/pypi/{_DIST}/json"
26
27
 
27
28
 
29
+ def _with_extras(target: str) -> str:
30
+ """Attach the ``[ui]`` extra so a self-update never drops the control-panel
31
+ deps — the reason ``agentos-ui`` broke (``No module named 'starlette'``) after
32
+ each upgrade. Cross-platform: the pip target syntax is identical on Linux and
33
+ Windows.
34
+
35
+ * bare dist name → ``devops-bot-sdk[ui]``
36
+ * git+/URL direct ref → ``devops-bot-sdk[ui] @ git+…`` (PEP 508)
37
+ Already-tagged targets are returned unchanged.
38
+ """
39
+ if _EXTRAS in target:
40
+ return target
41
+ if "://" in target: # a VCS/URL install (git+ssh / git+https)
42
+ return f"{_DIST}{_EXTRAS} @ {target}"
43
+ if target == _DIST: # bare PyPI dist
44
+ return f"{_DIST}{_EXTRAS}"
45
+ return target
46
+
47
+
28
48
  # ── Platform helpers ──────────────────────────────────────────────────────────
29
49
 
30
50
  def _pip_flags() -> list[str]:
@@ -197,7 +217,9 @@ def _install_candidates() -> list[str]:
197
217
  if gh_token:
198
218
  cands.append(f"git+https://{gh_token}@github.com/{_GITHUB_REPO}.git@{branch}")
199
219
  cands.append(_DIST)
200
- return cands
220
+ # Every target carries the [ui] extra so the control-panel deps are reinstalled
221
+ # on each upgrade (previously they were dropped, breaking `agentos-ui`).
222
+ return _unique([_with_extras(c) for c in cands])
201
223
 
202
224
 
203
225
  def _pip_install(url: str, *, force: bool = False) -> bool:
@@ -316,7 +338,7 @@ def auto_update_and_reexec(module: str, argv: list[str]) -> None:
316
338
  "error: the SDK self-update left the install broken and auto-repair "
317
339
  "failed.\n reinstall it manually, then re-run agentos-auto:\n"
318
340
  f' "{sys.executable}" -m pip install --force-reinstall '
319
- f'"git+https://github.com/{_GITHUB_REPO}.git@{_sdk_branch()}"'
341
+ f'"devops-bot-sdk[ui] @ git+https://github.com/{_GITHUB_REPO}.git@{_sdk_branch()}"'
320
342
  )
321
343
  sys.exit(1)
322
344
  print("✓ repaired.")
@@ -0,0 +1,53 @@
1
+ """SDK VPS health — collect_health() + GET /api/ui/health.
2
+
3
+ Resource metrics degrade gracefully when psutil is absent (resources.available
4
+ False), and the route is token-gated like every /api/ui/* endpoint.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import sys
9
+ from unittest.mock import patch
10
+
11
+ import pytest
12
+
13
+ from sdk.ui.health import collect_health
14
+
15
+
16
+ class TestCollectHealth:
17
+ def test_shape(self):
18
+ h = collect_health()
19
+ assert h["status"] == "ok"
20
+ assert h["sdk_version"] and "host" in h and "resources" in h and "loop" in h
21
+ assert "os" in h["host"] and "python" in h["host"]
22
+
23
+ def test_resources_degrade_without_psutil(self):
24
+ # Simulate psutil missing → resources.available False, no crash.
25
+ with patch.dict(sys.modules, {"psutil": None}):
26
+ h = collect_health()
27
+ assert h["resources"]["available"] is False
28
+
29
+ def test_loop_never_breaks_probe(self):
30
+ with patch("sdk.ui.runner.loop_state", side_effect=RuntimeError("boom")):
31
+ h = collect_health()
32
+ assert "error" in h["loop"]
33
+
34
+
35
+ fastapi = pytest.importorskip("fastapi")
36
+ from fastapi.testclient import TestClient # noqa: E402
37
+
38
+ from sdk.ui.api import require_token, router # noqa: E402
39
+
40
+
41
+ def _client():
42
+ app = fastapi.FastAPI()
43
+ app.include_router(router)
44
+ app.dependency_overrides[require_token] = lambda: None
45
+ return TestClient(app)
46
+
47
+
48
+ class TestHealthRoute:
49
+ def test_returns_health(self):
50
+ resp = _client().get("/api/ui/health")
51
+ assert resp.status_code == 200
52
+ body = resp.json()
53
+ assert body["status"] == "ok" and "resources" in body and "loop" in body
@@ -0,0 +1,49 @@
1
+ """Regression: self-update must keep the ``[ui]`` extra so the control panel's
2
+ web deps (fastapi/uvicorn/starlette) are reinstalled on every upgrade.
3
+
4
+ Before this, ``agentos-auto update`` reinstalled ``devops-bot-sdk`` WITHOUT the
5
+ extra, so ``agentos-ui`` broke with ``No module named 'starlette'`` after each
6
+ update and had to be `pip install`ed by hand.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from unittest.mock import patch
11
+
12
+ from sdk import updater
13
+
14
+
15
+ class TestWithExtras:
16
+ def test_bare_dist_gets_extra(self):
17
+ assert updater._with_extras("devops-bot-sdk") == "devops-bot-sdk[ui]"
18
+
19
+ def test_git_ssh_uses_pep508_direct_ref(self):
20
+ url = "git+ssh://git@github.com/consultancy-outfit/DevOpsBot-SDK.git@main"
21
+ assert updater._with_extras(url) == f"devops-bot-sdk[ui] @ {url}"
22
+
23
+ def test_git_https_token_preserved(self):
24
+ url = "git+https://TOKEN@github.com/consultancy-outfit/DevOpsBot-SDK.git@main"
25
+ assert updater._with_extras(url) == f"devops-bot-sdk[ui] @ {url}"
26
+
27
+ def test_idempotent_bare(self):
28
+ assert updater._with_extras("devops-bot-sdk[ui]") == "devops-bot-sdk[ui]"
29
+
30
+ def test_idempotent_url(self):
31
+ tagged = "devops-bot-sdk[ui] @ git+ssh://git@github.com/x/y.git"
32
+ assert updater._with_extras(tagged) == tagged
33
+
34
+
35
+ class TestInstallCandidates:
36
+ def test_every_candidate_carries_the_ui_extra(self):
37
+ # No GitHub token → the SSH + detected + PyPI candidates only.
38
+ with patch.object(updater, "_fetch_github_token", return_value=None), \
39
+ patch.object(updater, "_detect_install_url", return_value=updater._DIST):
40
+ cands = updater._install_candidates()
41
+ assert cands, "expected at least one install candidate"
42
+ assert all("[ui]" in c for c in cands), cands
43
+
44
+ def test_token_https_candidate_also_tagged(self):
45
+ with patch.object(updater, "_fetch_github_token", return_value="gho_x"), \
46
+ patch.object(updater, "_detect_install_url", return_value=updater._DIST):
47
+ cands = updater._install_candidates()
48
+ https = [c for c in cands if "https://gho_x@" in c]
49
+ assert https and all(c.startswith("devops-bot-sdk[ui] @ ") for c in https), cands