devops-bot-sdk 1.4.145__tar.gz → 1.4.147__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.
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/inventory.py +2 -2
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/local_exec.py +76 -2
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/service.py +200 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/setup_manager.py +14 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/updater.py +36 -1
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/README.md +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/pyproject.toml +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/client.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/setup.cfg +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/tests/test_updater_extras.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
|
|
2
2
|
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.147
|
|
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.
|
|
33
|
+
__version__ = "1.4.147" # 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
|
|
@@ -11,7 +11,6 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import json
|
|
13
13
|
import logging
|
|
14
|
-
import shutil
|
|
15
14
|
import subprocess
|
|
16
15
|
from pathlib import Path
|
|
17
16
|
|
|
@@ -70,7 +69,8 @@ def _connectors() -> list[dict]:
|
|
|
70
69
|
|
|
71
70
|
def _mcp_servers() -> list[dict]:
|
|
72
71
|
"""Installed MCP servers / Claude plugins, parsed from ``claude mcp list``."""
|
|
73
|
-
|
|
72
|
+
from sdk.local_exec import claude_cli_path
|
|
73
|
+
cli = claude_cli_path()
|
|
74
74
|
if not cli:
|
|
75
75
|
return []
|
|
76
76
|
try:
|
|
@@ -22,6 +22,7 @@ import json
|
|
|
22
22
|
import os
|
|
23
23
|
import re
|
|
24
24
|
import shutil
|
|
25
|
+
import subprocess
|
|
25
26
|
import sys
|
|
26
27
|
from datetime import datetime, timedelta
|
|
27
28
|
from pathlib import Path
|
|
@@ -240,7 +241,21 @@ EventHandler = Callable[[dict], Awaitable[None]]
|
|
|
240
241
|
|
|
241
242
|
|
|
242
243
|
def claude_cli_path() -> str | None:
|
|
243
|
-
"""Absolute path to the `claude` CLI if
|
|
244
|
+
"""Absolute path to the `claude` CLI, or None if it isn't on PATH.
|
|
245
|
+
|
|
246
|
+
Resolution order:
|
|
247
|
+
1. ``AGENTOS_CLAUDE_CLI`` env var — explicit path override (escape hatch for
|
|
248
|
+
an install in a non-standard location).
|
|
249
|
+
2. ``PATH`` (``shutil.which``) — cross-platform, no guessing.
|
|
250
|
+
|
|
251
|
+
The service must therefore RUN with a PATH that includes the CLI. The install
|
|
252
|
+
step bakes that in (see ``sdk.ui.service`` — systemd ``Environment=PATH`` /
|
|
253
|
+
launchd ``EnvironmentVariables``; Windows tasks inherit the user's PATH), so
|
|
254
|
+
``engine=claude_code`` works without anyone editing a unit file.
|
|
255
|
+
"""
|
|
256
|
+
override = os.environ.get("AGENTOS_CLAUDE_CLI", "").strip()
|
|
257
|
+
if override and Path(override).exists():
|
|
258
|
+
return override
|
|
244
259
|
return shutil.which("claude")
|
|
245
260
|
|
|
246
261
|
|
|
@@ -249,6 +264,54 @@ def is_available() -> bool:
|
|
|
249
264
|
return claude_cli_path() is not None
|
|
250
265
|
|
|
251
266
|
|
|
267
|
+
def claude_status(timeout: float = 15.0) -> dict:
|
|
268
|
+
"""Readiness of the local Claude Code CLI: installed AND logged in.
|
|
269
|
+
|
|
270
|
+
Runs ``claude auth status`` (which prints structured JSON) so the panel /
|
|
271
|
+
health surface can show a non-technical operator one clear signal — "connected
|
|
272
|
+
as you@org" vs "not logged in" — instead of a raw CLI error surfacing only when
|
|
273
|
+
a job runs. All failures degrade to ``logged_in: False`` with an ``error`` note;
|
|
274
|
+
this never raises.
|
|
275
|
+
|
|
276
|
+
Returns::
|
|
277
|
+
|
|
278
|
+
{"installed": bool, "logged_in": bool, "path": str|None,
|
|
279
|
+
"auth_method": str|None, "email": str|None, "org": str|None,
|
|
280
|
+
"error": str|None}
|
|
281
|
+
"""
|
|
282
|
+
cli = claude_cli_path()
|
|
283
|
+
if not cli:
|
|
284
|
+
return {"installed": False, "logged_in": False, "path": None,
|
|
285
|
+
"auth_method": None, "email": None, "org": None,
|
|
286
|
+
"error": "Claude Code CLI not found. Install it or set AGENTOS_CLAUDE_CLI."}
|
|
287
|
+
try:
|
|
288
|
+
proc = subprocess.run([cli, "auth", "status"], capture_output=True,
|
|
289
|
+
text=True, timeout=timeout)
|
|
290
|
+
except Exception as exc: # noqa: BLE001 — timeout / OSError; never raise to caller
|
|
291
|
+
return {"installed": True, "logged_in": False, "path": cli, "auth_method": None,
|
|
292
|
+
"email": None, "org": None, "error": f"`claude auth status` failed: {exc}"}
|
|
293
|
+
|
|
294
|
+
out = (proc.stdout or "").strip()
|
|
295
|
+
try:
|
|
296
|
+
data = json.loads(out)
|
|
297
|
+
except Exception: # noqa: BLE001 — older CLIs may print human text, not JSON
|
|
298
|
+
logged = "logged in" in out.lower()
|
|
299
|
+
return {"installed": True, "logged_in": logged, "path": cli, "auth_method": None,
|
|
300
|
+
"email": None, "org": None,
|
|
301
|
+
"error": None if logged else ((proc.stderr or out or "not logged in").strip()[:200])}
|
|
302
|
+
|
|
303
|
+
logged = bool(data.get("loggedIn"))
|
|
304
|
+
return {
|
|
305
|
+
"installed": True,
|
|
306
|
+
"logged_in": logged,
|
|
307
|
+
"path": cli,
|
|
308
|
+
"auth_method": data.get("authMethod"),
|
|
309
|
+
"email": data.get("email"),
|
|
310
|
+
"org": data.get("orgName") or data.get("orgId"),
|
|
311
|
+
"error": None if logged else "Not logged in — run `claude` on the VPS to sign in.",
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
252
315
|
# Cache the discovered MCP allow-entries for the process lifetime — the set of
|
|
253
316
|
# configured servers doesn't change mid-run, and `claude mcp list` does a network
|
|
254
317
|
# health check (~seconds) we don't want to repeat per ticket.
|
|
@@ -396,7 +459,9 @@ async def run_claude_local(
|
|
|
396
459
|
cli = claude_cli_path()
|
|
397
460
|
if not cli:
|
|
398
461
|
return {
|
|
399
|
-
"ok": False, "result": "",
|
|
462
|
+
"ok": False, "result": "",
|
|
463
|
+
"error": ("Claude Code CLI not found. Install it, or if it is installed "
|
|
464
|
+
"in a non-standard location set AGENTOS_CLAUDE_CLI to its full path."),
|
|
400
465
|
"input_tokens": 0, "output_tokens": 0, "total_tokens": 0,
|
|
401
466
|
"cost_usd": 0.0, "files_changed": [],
|
|
402
467
|
}
|
|
@@ -461,6 +526,15 @@ async def run_claude_local(
|
|
|
461
526
|
# the user's global git config. Tokens stay in the subprocess env only.
|
|
462
527
|
env = dict(os.environ)
|
|
463
528
|
|
|
529
|
+
# Make sure the CLI's own directory is on the subprocess PATH: node/gh and the
|
|
530
|
+
# CLI's sibling tools usually live alongside it, and under a minimal service
|
|
531
|
+
# PATH they'd otherwise be missing. Prepend the resolved binary's dir only —
|
|
532
|
+
# no guessing beyond that — so anything the operator exported still wins.
|
|
533
|
+
_cli_dir = str(Path(cli).resolve().parent)
|
|
534
|
+
_cur_path = env.get("PATH", "")
|
|
535
|
+
if _cli_dir not in _cur_path.split(os.pathsep):
|
|
536
|
+
env["PATH"] = _cli_dir + (os.pathsep + _cur_path if _cur_path else "")
|
|
537
|
+
|
|
464
538
|
# A custom agent's persisted API keys (sdk.agents.secrets) are overlaid here
|
|
465
539
|
# so they reach the subprocess (and any MCP servers it spawns) WITHOUT ever
|
|
466
540
|
# touching the ambient os.environ of this process — same "subprocess env only"
|
|
@@ -17,6 +17,7 @@ Everything is best-effort and returns ``(ok, message)`` so the caller (the
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import os
|
|
20
|
+
import re
|
|
20
21
|
import shutil
|
|
21
22
|
import subprocess
|
|
22
23
|
import sys
|
|
@@ -51,6 +52,30 @@ def _systemd_available() -> bool:
|
|
|
51
52
|
return bool(shutil.which("systemctl")) and Path("/run/systemd/system").exists()
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
def _service_path_env() -> str:
|
|
56
|
+
"""PATH for the service environment.
|
|
57
|
+
|
|
58
|
+
A service (systemd/launchd) runs with a minimal PATH that omits ~/.local/bin,
|
|
59
|
+
nvm/Homebrew, etc. — exactly where the ``claude`` CLI and its node/git/gh
|
|
60
|
+
runtime live. Prepend the resolved CLI's own directory and ~/.local/bin to the
|
|
61
|
+
standard system dirs so ``engine=claude_code`` works with no manual PATH edits.
|
|
62
|
+
"""
|
|
63
|
+
base = ["/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"]
|
|
64
|
+
extra = [str(Path.home() / ".local" / "bin")]
|
|
65
|
+
try:
|
|
66
|
+
from sdk.local_exec import claude_cli_path
|
|
67
|
+
cli = claude_cli_path()
|
|
68
|
+
if cli:
|
|
69
|
+
extra.append(str(Path(cli).resolve().parent))
|
|
70
|
+
except Exception: # noqa: BLE001 — discovery must never break install
|
|
71
|
+
pass
|
|
72
|
+
ordered: list[str] = []
|
|
73
|
+
for d in extra + base:
|
|
74
|
+
if d and d not in ordered:
|
|
75
|
+
ordered.append(d)
|
|
76
|
+
return ":".join(ordered)
|
|
77
|
+
|
|
78
|
+
|
|
54
79
|
def _unit_text(host: str, port: int, home: str) -> str:
|
|
55
80
|
exec_line = f"{_exe()} --host {host} --port {port} --no-browser"
|
|
56
81
|
return (
|
|
@@ -64,6 +89,7 @@ def _unit_text(host: str, port: int, home: str) -> str:
|
|
|
64
89
|
"Restart=always\n"
|
|
65
90
|
"RestartSec=5\n"
|
|
66
91
|
f"Environment=HOME={home}\n"
|
|
92
|
+
f"Environment=PATH={_service_path_env()}\n"
|
|
67
93
|
f"WorkingDirectory={home}\n\n"
|
|
68
94
|
"[Install]\n"
|
|
69
95
|
f"WantedBy={{target}}\n"
|
|
@@ -145,6 +171,9 @@ def _install_launchd(host: str, port: int) -> tuple[bool, str]:
|
|
|
145
171
|
f' <string>{exe}</string><string>--host</string><string>{host}</string>\n'
|
|
146
172
|
f' <string>--port</string><string>{port}</string><string>--no-browser</string>\n'
|
|
147
173
|
' </array>\n'
|
|
174
|
+
' <key>EnvironmentVariables</key><dict>\n'
|
|
175
|
+
f' <key>PATH</key><string>{_service_path_env()}</string>\n'
|
|
176
|
+
' </dict>\n'
|
|
148
177
|
' <key>RunAtLoad</key><true/>\n'
|
|
149
178
|
' <key>KeepAlive</key><true/>\n'
|
|
150
179
|
'</dict></plist>\n'
|
|
@@ -257,3 +286,174 @@ def restart() -> tuple[bool, str]:
|
|
|
257
286
|
return (rc == 0), (f"restarted systemd service '{SERVICE_NAME}'." if rc == 0
|
|
258
287
|
else out or f"systemctl restart {SERVICE_NAME} failed")
|
|
259
288
|
return False, "no supported service manager found — restart the panel manually (`agentos-ui`)."
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _systemd_unit_candidates() -> list[Path]:
|
|
292
|
+
"""Both places a systemd unit can live, system scope first."""
|
|
293
|
+
return [
|
|
294
|
+
Path("/etc/systemd/system") / f"{SERVICE_NAME}.service",
|
|
295
|
+
Path.home() / ".config" / "systemd" / "user" / f"{SERVICE_NAME}.service",
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _existing_systemd_unit() -> Path | None:
|
|
300
|
+
"""The installed unit file, checking BOTH system and user scope regardless of
|
|
301
|
+
the current privilege — so a non-root ``agentos-auto update`` still finds a
|
|
302
|
+
system service installed via ``sudo agentos-ui install`` (and vice-versa)."""
|
|
303
|
+
for p in _systemd_unit_candidates():
|
|
304
|
+
try:
|
|
305
|
+
if p.exists():
|
|
306
|
+
return p
|
|
307
|
+
except OSError:
|
|
308
|
+
continue
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _service_definition_path() -> Path:
|
|
313
|
+
"""On-disk service definition — an EXISTING unit if there is one (either scope),
|
|
314
|
+
else the path we would CREATE at the current privilege."""
|
|
315
|
+
if IS_WINDOWS: # schtasks has no file; handled separately
|
|
316
|
+
return Path()
|
|
317
|
+
if IS_MAC:
|
|
318
|
+
return _mac_plist_path()
|
|
319
|
+
existing = _existing_systemd_unit()
|
|
320
|
+
if existing:
|
|
321
|
+
return existing
|
|
322
|
+
is_root = hasattr(os, "geteuid") and os.geteuid() == 0
|
|
323
|
+
base = Path("/etc/systemd/system") if is_root else Path.home() / ".config" / "systemd" / "user"
|
|
324
|
+
return base / f"{SERVICE_NAME}.service"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _needs_root_for_existing() -> bool:
|
|
328
|
+
"""True when a SYSTEM unit is installed but we're not root — so we must not
|
|
329
|
+
silently create a conflicting *user* unit; the caller should ask for sudo."""
|
|
330
|
+
if IS_WINDOWS or IS_MAC:
|
|
331
|
+
return False
|
|
332
|
+
is_root = hasattr(os, "geteuid") and os.geteuid() == 0
|
|
333
|
+
existing = _existing_systemd_unit()
|
|
334
|
+
return (existing is not None
|
|
335
|
+
and existing.parent == Path("/etc/systemd/system")
|
|
336
|
+
and not is_root)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def is_installed() -> bool:
|
|
340
|
+
"""True when a managed agentos-ui service/unit exists (any platform, any scope).
|
|
341
|
+
|
|
342
|
+
Best-effort and never raises — used by the updater to decide whether an upgrade
|
|
343
|
+
should create, regenerate, or restart the service.
|
|
344
|
+
"""
|
|
345
|
+
try:
|
|
346
|
+
if IS_WINDOWS:
|
|
347
|
+
rc, _ = _run(["schtasks", "/Query", "/TN", WIN_TASK])
|
|
348
|
+
return rc == 0
|
|
349
|
+
if IS_MAC:
|
|
350
|
+
return _mac_plist_path().exists()
|
|
351
|
+
return _existing_systemd_unit() is not None
|
|
352
|
+
except Exception: # noqa: BLE001
|
|
353
|
+
return False
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _is_active() -> bool:
|
|
357
|
+
"""True when the managed service is currently running (read-only query)."""
|
|
358
|
+
try:
|
|
359
|
+
if IS_WINDOWS:
|
|
360
|
+
_, out = _run(["schtasks", "/Query", "/TN", WIN_TASK, "/FO", "LIST", "/V"])
|
|
361
|
+
return "Running" in out
|
|
362
|
+
if IS_MAC:
|
|
363
|
+
rc, out = _run(["launchctl", "list", _MAC_LABEL])
|
|
364
|
+
return rc == 0 and bool(out.strip())
|
|
365
|
+
existing = _existing_systemd_unit()
|
|
366
|
+
user = existing is not None and existing.parent.name == "user"
|
|
367
|
+
sysctl = ["systemctl", "--user"] if user else ["systemctl"]
|
|
368
|
+
_, out = _run([*sysctl, "is-active", SERVICE_NAME])
|
|
369
|
+
return out.strip() == "active"
|
|
370
|
+
except Exception: # noqa: BLE001
|
|
371
|
+
return False
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _installed_host_port() -> tuple[str, int]:
|
|
375
|
+
"""Read the host/port the service was installed with, so a refresh preserves
|
|
376
|
+
them instead of resetting to the install() defaults. Falls back to 0.0.0.0:8765.
|
|
377
|
+
"""
|
|
378
|
+
host, port = "0.0.0.0", 8765
|
|
379
|
+
text = ""
|
|
380
|
+
try:
|
|
381
|
+
if IS_WINDOWS:
|
|
382
|
+
_, text = _run(["schtasks", "/Query", "/TN", WIN_TASK, "/V", "/FO", "LIST"])
|
|
383
|
+
else:
|
|
384
|
+
p = _service_definition_path()
|
|
385
|
+
text = p.read_text() if p and p.exists() else ""
|
|
386
|
+
except Exception: # noqa: BLE001
|
|
387
|
+
text = ""
|
|
388
|
+
clean = re.sub(r"<[^>]+>", " ", text) # flatten plist <string> tags to spaces
|
|
389
|
+
m = re.search(r"--host\s+(\S+)", clean)
|
|
390
|
+
if m:
|
|
391
|
+
host = m.group(1).strip().strip('"')
|
|
392
|
+
m = re.search(r"--port\s+(\d+)", clean)
|
|
393
|
+
if m:
|
|
394
|
+
port = int(m.group(1))
|
|
395
|
+
return host, port
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def refresh() -> tuple[bool, str]:
|
|
399
|
+
"""Regenerate the service definition and restart it after an SDK upgrade.
|
|
400
|
+
|
|
401
|
+
``agentos-auto update`` only pip-upgrades the package; the on-disk unit and the
|
|
402
|
+
running process are untouched. So a change to the generated unit (e.g. the
|
|
403
|
+
baked-in PATH that lets the service find the ``claude`` CLI) or to the code
|
|
404
|
+
never takes effect until this runs. Regenerating rewrites the unit with the
|
|
405
|
+
upgraded SDK's definition (preserving the installed host/port), and the restart
|
|
406
|
+
both applies the new unit and loads the new code.
|
|
407
|
+
|
|
408
|
+
No-op returning ``(False, msg)`` when no managed service is installed, so a
|
|
409
|
+
manually-launched panel is left alone.
|
|
410
|
+
"""
|
|
411
|
+
if not is_installed():
|
|
412
|
+
return False, "no managed service installed — nothing to refresh."
|
|
413
|
+
if _needs_root_for_existing():
|
|
414
|
+
return False, ("the panel runs as a system service — re-run with sudo "
|
|
415
|
+
"(`sudo agentos-ui install`) to apply the update.")
|
|
416
|
+
host, port = _installed_host_port()
|
|
417
|
+
ok, msg = install(host, port) # rewrites unit/plist/task + daemon-reload
|
|
418
|
+
if not ok:
|
|
419
|
+
return False, f"could not regenerate service: {msg}"
|
|
420
|
+
# `enable --now` / launchctl load won't restart an ALREADY-running unit, so
|
|
421
|
+
# force a restart to apply the regenerated definition and load the new code.
|
|
422
|
+
rok, rmsg = restart()
|
|
423
|
+
return (True, f"service refreshed ({host}:{port}); {rmsg}" if rok
|
|
424
|
+
else f"service definition regenerated ({host}:{port}) but restart needs attention: {rmsg}")
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _service_disabled() -> bool:
|
|
428
|
+
return os.environ.get("AGENTOS_NO_SERVICE", "").strip().lower() in ("1", "true", "yes", "on")
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def ensure(restart_running: bool = False) -> tuple[bool, str]:
|
|
432
|
+
"""Make sure the panel service EXISTS and is running the current SDK.
|
|
433
|
+
|
|
434
|
+
This is what lets ``agentos-auto update`` yield a working 24/7 service without a
|
|
435
|
+
separate ``agentos-ui install`` step:
|
|
436
|
+
|
|
437
|
+
* not installed → create it (``install``)
|
|
438
|
+
* installed + code changed → ``refresh`` (regenerate unit + restart)
|
|
439
|
+
(pass ``restart_running=True`` after a real upgrade)
|
|
440
|
+
* installed + unchanged + running → leave it alone
|
|
441
|
+
* installed + unchanged + stopped → start it
|
|
442
|
+
|
|
443
|
+
Best-effort; opt out entirely with ``AGENTOS_NO_SERVICE=1`` (e.g. a box that
|
|
444
|
+
runs the panel manually or only uses the SDK as a library).
|
|
445
|
+
"""
|
|
446
|
+
if _service_disabled():
|
|
447
|
+
return False, "AGENTOS_NO_SERVICE set — leaving the service unmanaged."
|
|
448
|
+
if not is_installed():
|
|
449
|
+
ok, msg = install() # create at the current privilege
|
|
450
|
+
return ok, (f"service created — {msg}" if ok else f"could not create service: {msg}")
|
|
451
|
+
if _needs_root_for_existing():
|
|
452
|
+
return False, ("a system service is installed — run `sudo agentos-ui install` "
|
|
453
|
+
"to apply updates and manage it.")
|
|
454
|
+
if restart_running:
|
|
455
|
+
return refresh()
|
|
456
|
+
if _is_active():
|
|
457
|
+
return True, "service already running."
|
|
458
|
+
rok, rmsg = restart() # installed but down → bring it back up
|
|
459
|
+
return rok, (f"service was down — started; {rmsg}" if rok else rmsg)
|
|
@@ -64,6 +64,19 @@ def sdk_version() -> str:
|
|
|
64
64
|
return "unknown"
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def claude_readiness() -> dict:
|
|
68
|
+
"""Claude Code CLI readiness (installed + logged in) for the /status surface.
|
|
69
|
+
|
|
70
|
+
Guarded: any failure degrades to a not-ready dict rather than breaking /status.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
from sdk.local_exec import claude_status
|
|
74
|
+
return claude_status()
|
|
75
|
+
except Exception as exc: # noqa: BLE001
|
|
76
|
+
return {"installed": False, "logged_in": False, "path": None,
|
|
77
|
+
"auth_method": None, "email": None, "org": None, "error": str(exc)}
|
|
78
|
+
|
|
79
|
+
|
|
67
80
|
def environment_info() -> dict:
|
|
68
81
|
"""Env snapshot for the /status endpoint and setup wizard."""
|
|
69
82
|
vp = running_venv()
|
|
@@ -78,6 +91,7 @@ def environment_info() -> dict:
|
|
|
78
91
|
"expected_agentsystem_root": str(root),
|
|
79
92
|
"agentsystem_exists": root.exists(),
|
|
80
93
|
"sdk_version": sdk_version(),
|
|
94
|
+
"claude": claude_readiness(),
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
|
|
@@ -273,6 +273,37 @@ def self_update() -> bool:
|
|
|
273
273
|
return False
|
|
274
274
|
|
|
275
275
|
|
|
276
|
+
def _ensure_service(quiet: bool = False, *, restart: bool = False) -> None:
|
|
277
|
+
"""Ensure the panel service exists and runs the current SDK, after an update check.
|
|
278
|
+
|
|
279
|
+
Without this, ``agentos-auto update`` pip-upgrades the code but leaves the old
|
|
280
|
+
systemd unit / launchd plist and the old running process in place — so unit
|
|
281
|
+
changes (e.g. the baked-in PATH that lets the service find ``claude``) and new
|
|
282
|
+
code silently never take effect, and a box that never ran ``agentos-ui install``
|
|
283
|
+
has no service at all. ``service.ensure`` creates it when missing, refreshes it
|
|
284
|
+
after a real upgrade (``restart=True``), and starts it if it's down — so a plain
|
|
285
|
+
``agentos-auto update`` reliably leaves ``systemctl status agentos-ui`` running.
|
|
286
|
+
|
|
287
|
+
Best-effort and never raises. Silent on a box without the ``[ui]`` extra (no
|
|
288
|
+
panel to manage) and opt-out via ``AGENTOS_NO_SERVICE=1``.
|
|
289
|
+
|
|
290
|
+
Safe here because ``auto_update`` runs from the short-lived ``agentos-auto``
|
|
291
|
+
process, not inside the long-running panel — restarting the panel can't kill
|
|
292
|
+
the updater mid-run.
|
|
293
|
+
"""
|
|
294
|
+
try:
|
|
295
|
+
from sdk.ui import service
|
|
296
|
+
except Exception: # noqa: BLE001 — no [ui] extra installed → no panel service
|
|
297
|
+
return
|
|
298
|
+
try:
|
|
299
|
+
ok, msg = service.ensure(restart_running=restart)
|
|
300
|
+
if not quiet and msg:
|
|
301
|
+
print(f"↻ {msg}" if ok else f"(service: {msg})")
|
|
302
|
+
except Exception as exc: # noqa: BLE001 — an update must never fail on the service step
|
|
303
|
+
if not quiet:
|
|
304
|
+
print(f"(service check failed: {exc})")
|
|
305
|
+
|
|
306
|
+
|
|
276
307
|
def auto_update(quiet: bool = False) -> tuple[bool, str, str | None]:
|
|
277
308
|
"""Check for a newer version and upgrade if one exists.
|
|
278
309
|
|
|
@@ -292,13 +323,16 @@ def auto_update(quiet: bool = False) -> tuple[bool, str, str | None]:
|
|
|
292
323
|
if not is_github:
|
|
293
324
|
latest = latest_pypi_version()
|
|
294
325
|
if not latest or _vtuple(latest) <= _vtuple(cur):
|
|
326
|
+
_ensure_service(quiet, restart=False) # already current — just self-heal the service
|
|
295
327
|
return (False, cur, latest)
|
|
296
328
|
if not quiet:
|
|
297
329
|
print(f"updating {_DIST} {cur} → {latest} …")
|
|
298
330
|
ok = self_update()
|
|
299
331
|
if not quiet and ok:
|
|
300
332
|
print("✓ updated.")
|
|
301
|
-
#
|
|
333
|
+
# Ensure the service exists + runs the new code (restart only on a real
|
|
334
|
+
# upgrade); also self-heals a missing/down service when already current.
|
|
335
|
+
_ensure_service(quiet, restart=ok)
|
|
302
336
|
return (ok, cur, latest if ok else None)
|
|
303
337
|
|
|
304
338
|
# GitHub path
|
|
@@ -313,6 +347,7 @@ def auto_update(quiet: bool = False) -> tuple[bool, str, str | None]:
|
|
|
313
347
|
elif ok:
|
|
314
348
|
print(f"✓ already up to date (v{cur}).")
|
|
315
349
|
# silent on failure — continue on the existing version without noise
|
|
350
|
+
_ensure_service(quiet, restart=updated)
|
|
316
351
|
return (updated, cur, after if updated else None)
|
|
317
352
|
|
|
318
353
|
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.145 → devops_bot_sdk-1.4.147}/devops_bot_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|