devops-bot-sdk 1.4.29__tar.gz → 1.4.31__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.29 → devops_bot_sdk-1.4.31}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/client.py +28 -1
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/run_auto.py +114 -16
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/README.md +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/setup.cfg +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devops-bot-sdk"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.31"
|
|
8
8
|
description = "DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "LicenseRef-Proprietary"
|
|
@@ -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.31
|
|
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.31"
|
|
34
34
|
__author__ = "AgentOS"
|
|
35
35
|
|
|
36
36
|
from sdk.client import BackendClient
|
|
@@ -36,7 +36,7 @@ from sdk.sse import _check_status, stream_with_reconnect
|
|
|
36
36
|
|
|
37
37
|
logger = logging.getLogger(__name__)
|
|
38
38
|
|
|
39
|
-
SDK_VERSION = "1.4.
|
|
39
|
+
SDK_VERSION = "1.4.31"
|
|
40
40
|
_POLL_INTERVAL = 3.0
|
|
41
41
|
_POLL_TIMEOUT = 600.0
|
|
42
42
|
_ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
|
|
@@ -691,6 +691,33 @@ class BackendClient:
|
|
|
691
691
|
logger.warning("client.move_to_backlog error task=%s err=%s", task_id, exc)
|
|
692
692
|
return False
|
|
693
693
|
|
|
694
|
+
async def move_to_todo(self, task_id: str) -> bool:
|
|
695
|
+
"""PUT {BE}/tasks-ml/{id} status="To Do" (best-effort).
|
|
696
|
+
|
|
697
|
+
Used by `agentos-auto stop` to return the in-flight ticket to the unstarted
|
|
698
|
+
queue so a later run re-picks it up from scratch (rather than leaving it
|
|
699
|
+
In Progress). Returns True on success; logs and returns False otherwise.
|
|
700
|
+
"""
|
|
701
|
+
from sdk.config import BE_DEFAULT_URL
|
|
702
|
+
|
|
703
|
+
url = f"{BE_DEFAULT_URL.rstrip('/')}/tasks-ml/{task_id}"
|
|
704
|
+
headers = {
|
|
705
|
+
k: v for k, v in self._headers.items() if k.lower() != "content-type"
|
|
706
|
+
}
|
|
707
|
+
headers.update({"accept": "*/*", "developer-name": "null", "product": ""})
|
|
708
|
+
try:
|
|
709
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
710
|
+
resp = await client.put(url, files={"status": (None, "To Do")}, headers=headers)
|
|
711
|
+
if resp.status_code >= 400:
|
|
712
|
+
logger.warning("client.move_to_todo failed task=%s status=%s body=%s",
|
|
713
|
+
task_id, resp.status_code, resp.text[:200])
|
|
714
|
+
return False
|
|
715
|
+
await self.log_activity(f"Task {task_id}: status → To Do")
|
|
716
|
+
return True
|
|
717
|
+
except Exception as exc: # noqa: BLE001
|
|
718
|
+
logger.warning("client.move_to_todo error task=%s err=%s", task_id, exc)
|
|
719
|
+
return False
|
|
720
|
+
|
|
694
721
|
async def mark_in_progress(self, task_id: str) -> bool:
|
|
695
722
|
"""PUT {BE}/tasks-ml/{id} status=In Progress (best-effort).
|
|
696
723
|
|
|
@@ -28,11 +28,11 @@ Auto-update: on `run`/`resume`, the SDK checks for a newer version and upgrades
|
|
|
28
28
|
itself (then re-runs on the new code) BEFORE detaching, so non-technical users
|
|
29
29
|
never reinstall.
|
|
30
30
|
|
|
31
|
-
Stop
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
whatever is still To Do.
|
|
31
|
+
Stop model: `stop` is immediate — it kills the loop's whole process group (the
|
|
32
|
+
daemon and any child `claude`) and returns the in-flight ticket to "To Do" so a
|
|
33
|
+
later run restarts it cleanly, rather than pausing after the current task. The
|
|
34
|
+
tracked in-flight ticket id is kept in ~/.agentos/auto.current. `resume` (or just
|
|
35
|
+
re-running) starts fresh over whatever is still To Do.
|
|
36
36
|
"""
|
|
37
37
|
from __future__ import annotations
|
|
38
38
|
|
|
@@ -46,6 +46,9 @@ _AGENTOS_DIR = Path.home() / ".agentos"
|
|
|
46
46
|
_STOP_FILE = _AGENTOS_DIR / "auto.stop"
|
|
47
47
|
_PID_FILE = _AGENTOS_DIR / "auto.pid"
|
|
48
48
|
_LOG_FILE = _AGENTOS_DIR / "auto.log"
|
|
49
|
+
# Backend id (UUID) of the ticket currently being processed. `stop` reads it to
|
|
50
|
+
# return the in-flight ticket to "To Do" before killing the loop.
|
|
51
|
+
_CURRENT_FILE = _AGENTOS_DIR / "auto.current"
|
|
49
52
|
|
|
50
53
|
|
|
51
54
|
def _stop_requested() -> bool:
|
|
@@ -64,6 +67,29 @@ def _clear_stop() -> None:
|
|
|
64
67
|
pass
|
|
65
68
|
|
|
66
69
|
|
|
70
|
+
def _write_current(task_id: str) -> None:
|
|
71
|
+
try:
|
|
72
|
+
_AGENTOS_DIR.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
_CURRENT_FILE.write_text(task_id)
|
|
74
|
+
except Exception: # noqa: BLE001
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_current() -> str | None:
|
|
79
|
+
try:
|
|
80
|
+
val = _CURRENT_FILE.read_text().strip()
|
|
81
|
+
return val or None
|
|
82
|
+
except FileNotFoundError:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _clear_current() -> None:
|
|
87
|
+
try:
|
|
88
|
+
_CURRENT_FILE.unlink()
|
|
89
|
+
except FileNotFoundError:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
67
93
|
# ── Background-daemon support ─────────────────────────────────────────────
|
|
68
94
|
# `agentos-auto` must keep running after the terminal is closed and stop ONLY on
|
|
69
95
|
# `agentos-auto stop`. We fully detach from the controlling terminal (double-fork
|
|
@@ -157,9 +183,24 @@ def _parse_workspace_root(argv: list[str]) -> str | None:
|
|
|
157
183
|
return None
|
|
158
184
|
|
|
159
185
|
|
|
186
|
+
_current_task: str | None = None
|
|
187
|
+
|
|
188
|
+
|
|
160
189
|
async def _send(env: dict) -> None:
|
|
161
190
|
etype = env.get("type", "")
|
|
162
191
|
data = env.get("data", {}) or {}
|
|
192
|
+
|
|
193
|
+
# Track the in-flight ticket (backend UUID) so `stop` can return it to To Do.
|
|
194
|
+
# The handler tags per-task envelopes with jira_task_id / task_id (both = the
|
|
195
|
+
# backend UUID here). Persist only on change to avoid churning the file.
|
|
196
|
+
global _current_task
|
|
197
|
+
tid = data.get("jira_task_id") or data.get("task_id")
|
|
198
|
+
if tid and tid != _current_task:
|
|
199
|
+
_current_task = tid
|
|
200
|
+
_write_current(tid)
|
|
201
|
+
if etype == "done":
|
|
202
|
+
_clear_current()
|
|
203
|
+
|
|
163
204
|
if etype == "pipeline_started":
|
|
164
205
|
print(f"\n▶ orchestrate-auto (local) — {data.get('task_count', 0)} task(s)\n")
|
|
165
206
|
elif etype == "step_update":
|
|
@@ -213,12 +254,14 @@ def _run_loop(workspace_root: str | None) -> None:
|
|
|
213
254
|
print("\n⏸ interrupted — stop flag set. run `agentos-auto resume` to continue.")
|
|
214
255
|
finally:
|
|
215
256
|
# Whether the loop finished, paused, or crashed, drop our PID file so
|
|
216
|
-
# `status` doesn't report a dead daemon as running
|
|
257
|
+
# `status` doesn't report a dead daemon as running, and clear the
|
|
258
|
+
# in-flight marker (the queue is drained on a clean finish).
|
|
217
259
|
if _read_pid() == os.getpid():
|
|
218
260
|
try:
|
|
219
261
|
_PID_FILE.unlink()
|
|
220
262
|
except FileNotFoundError:
|
|
221
263
|
pass
|
|
264
|
+
_clear_current()
|
|
222
265
|
|
|
223
266
|
|
|
224
267
|
def main() -> None:
|
|
@@ -233,13 +276,63 @@ def main() -> None:
|
|
|
233
276
|
return
|
|
234
277
|
|
|
235
278
|
if cmd == "stop":
|
|
279
|
+
# Hard stop: halt the current task immediately, return it to To Do so it
|
|
280
|
+
# restarts cleanly next run, and kill the whole loop (not a cooperative
|
|
281
|
+
# pause after the current task).
|
|
236
282
|
_set_stop()
|
|
237
283
|
pid = _daemon_running()
|
|
284
|
+
cur = _read_current()
|
|
285
|
+
|
|
286
|
+
# 1. Kill the loop's ENTIRE process group first (daemon + any child
|
|
287
|
+
# `claude` it spawned) so nothing keeps mutating the ticket. The daemon
|
|
288
|
+
# is a session leader (setsid), so its PGID == its PID.
|
|
238
289
|
if pid:
|
|
239
|
-
|
|
240
|
-
|
|
290
|
+
import signal
|
|
291
|
+
import time as _t
|
|
292
|
+
# Kill the whole process group ONLY when pid is its own group leader
|
|
293
|
+
# (the daemon, via setsid) — that also reaps the child `claude`. For a
|
|
294
|
+
# foreground run (shares the shell's group) fall back to a single kill
|
|
295
|
+
# so we don't take down the user's terminal.
|
|
296
|
+
try:
|
|
297
|
+
is_group_leader = os.getpgid(pid) == pid
|
|
298
|
+
except ProcessLookupError:
|
|
299
|
+
is_group_leader = False
|
|
300
|
+
killed = False
|
|
301
|
+
for sig in (signal.SIGTERM, signal.SIGKILL):
|
|
302
|
+
try:
|
|
303
|
+
if is_group_leader:
|
|
304
|
+
os.killpg(pid, sig)
|
|
305
|
+
else:
|
|
306
|
+
os.kill(pid, sig)
|
|
307
|
+
except ProcessLookupError:
|
|
308
|
+
killed = True
|
|
309
|
+
break
|
|
310
|
+
except Exception: # noqa: BLE001
|
|
311
|
+
pass
|
|
312
|
+
_t.sleep(1.5)
|
|
313
|
+
if not _pid_alive(pid):
|
|
314
|
+
killed = True
|
|
315
|
+
break
|
|
316
|
+
print(f"stopped background loop (pid {pid})" if killed
|
|
317
|
+
else f"sent stop signals to pid {pid}")
|
|
241
318
|
else:
|
|
242
|
-
print("
|
|
319
|
+
print("no background loop running — stop flag set for any foreground run.")
|
|
320
|
+
|
|
321
|
+
# 2. Now that the loop is dead, move the in-flight ticket back to To Do.
|
|
322
|
+
if cur:
|
|
323
|
+
try:
|
|
324
|
+
from sdk.client import BackendClient
|
|
325
|
+
ok = asyncio.run(BackendClient.from_config().move_to_todo(cur))
|
|
326
|
+
print(f"in-flight ticket → To Do ({'ok' if ok else 'failed'})")
|
|
327
|
+
except Exception as exc: # noqa: BLE001
|
|
328
|
+
print(f"warning: could not move in-flight ticket to To Do ({exc})")
|
|
329
|
+
|
|
330
|
+
# 3. Clean up state files.
|
|
331
|
+
for f in (_PID_FILE, _CURRENT_FILE):
|
|
332
|
+
try:
|
|
333
|
+
f.unlink()
|
|
334
|
+
except FileNotFoundError:
|
|
335
|
+
pass
|
|
243
336
|
return
|
|
244
337
|
|
|
245
338
|
if cmd == "status":
|
|
@@ -247,12 +340,24 @@ def main() -> None:
|
|
|
247
340
|
if pid:
|
|
248
341
|
print(f"background loop: running (pid {pid})")
|
|
249
342
|
print(f"logs: {_LOG_FILE}")
|
|
343
|
+
cur = _read_current()
|
|
344
|
+
if cur:
|
|
345
|
+
print(f"current ticket: {cur}")
|
|
250
346
|
else:
|
|
251
347
|
print("background loop: not running")
|
|
252
348
|
print("stop pending: yes" if _stop_requested() else "stop pending: no")
|
|
253
349
|
return
|
|
254
350
|
|
|
255
351
|
if cmd in ("run", "resume"):
|
|
352
|
+
# Self-update FIRST (before the already-running check) so the installed
|
|
353
|
+
# package is refreshed even when a daemon is already running — otherwise a
|
|
354
|
+
# long-lived old daemon would pin `stop`/`status` to a stale version. Runs
|
|
355
|
+
# in the FOREGROUND so the user sees update output; re-execs on the new
|
|
356
|
+
# code. Skipped with --no-update or AGENTOS_NO_UPDATE=1.
|
|
357
|
+
if "--no-update" not in argv:
|
|
358
|
+
from sdk.updater import auto_update_and_reexec
|
|
359
|
+
auto_update_and_reexec("sdk.run_auto", sys.argv[1:])
|
|
360
|
+
|
|
256
361
|
# Refuse to start a second loop — one background daemon owns the To Do queue.
|
|
257
362
|
pid = _daemon_running()
|
|
258
363
|
if pid:
|
|
@@ -261,13 +366,6 @@ def main() -> None:
|
|
|
261
366
|
print(" stop: agentos-auto stop")
|
|
262
367
|
return
|
|
263
368
|
|
|
264
|
-
# Keep non-technical users current: self-update, then re-run on the new
|
|
265
|
-
# code. Runs in the FOREGROUND (before detaching) so the user sees update
|
|
266
|
-
# output. Skipped with --no-update or AGENTOS_NO_UPDATE=1.
|
|
267
|
-
if "--no-update" not in argv:
|
|
268
|
-
from sdk.updater import auto_update_and_reexec
|
|
269
|
-
auto_update_and_reexec("sdk.run_auto", sys.argv[1:])
|
|
270
|
-
|
|
271
369
|
workspace_root = _parse_workspace_root(argv)
|
|
272
370
|
|
|
273
371
|
# Background by default: detach so closing the terminal (SIGHUP) can't kill
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.29 → devops_bot_sdk-1.4.31}/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
|