devops-bot-sdk 1.4.17__tar.gz → 1.4.20__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.17 → devops_bot_sdk-1.4.20}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/client.py +56 -1
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/ipc/handlers.py +93 -3
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/local_exec.py +14 -2
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/updater.py +72 -31
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/README.md +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/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.20"
|
|
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.20
|
|
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.20"
|
|
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.20"
|
|
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
|
|
@@ -685,11 +685,66 @@ class BackendClient:
|
|
|
685
685
|
logger.warning("client.move_to_backlog failed task=%s status=%s body=%s",
|
|
686
686
|
task_id, resp.status_code, resp.text[:200])
|
|
687
687
|
return False
|
|
688
|
+
await self.log_activity(f"Task {task_id}: status → Backlog")
|
|
688
689
|
return True
|
|
689
690
|
except Exception as exc: # noqa: BLE001
|
|
690
691
|
logger.warning("client.move_to_backlog error task=%s err=%s", task_id, exc)
|
|
691
692
|
return False
|
|
692
693
|
|
|
694
|
+
async def mark_in_progress(self, task_id: str) -> bool:
|
|
695
|
+
"""PUT {BE}/tasks-ml/{id} status=In Progress (best-effort).
|
|
696
|
+
|
|
697
|
+
Called at the start of processing to move the ticket from To Do to
|
|
698
|
+
In Progress. Returns True on success; logs and returns False otherwise.
|
|
699
|
+
"""
|
|
700
|
+
from sdk.config import BE_DEFAULT_URL
|
|
701
|
+
|
|
702
|
+
url = f"{BE_DEFAULT_URL.rstrip('/')}/tasks-ml/{task_id}"
|
|
703
|
+
headers = {
|
|
704
|
+
k: v for k, v in self._headers.items() if k.lower() != "content-type"
|
|
705
|
+
}
|
|
706
|
+
headers.update({"accept": "*/*", "developer-name": "null", "product": ""})
|
|
707
|
+
try:
|
|
708
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
709
|
+
resp = await client.put(url, files={"status": (None, "In Progress")}, headers=headers)
|
|
710
|
+
if resp.status_code >= 400:
|
|
711
|
+
logger.warning("client.mark_in_progress failed task=%s status=%s body=%s",
|
|
712
|
+
task_id, resp.status_code, resp.text[:200])
|
|
713
|
+
return False
|
|
714
|
+
await self.log_activity(f"Task {task_id}: status → In Progress")
|
|
715
|
+
return True
|
|
716
|
+
except Exception as exc: # noqa: BLE001
|
|
717
|
+
logger.warning("client.mark_in_progress error task=%s err=%s", task_id, exc)
|
|
718
|
+
return False
|
|
719
|
+
|
|
720
|
+
async def log_activity(self, activity: str) -> bool:
|
|
721
|
+
"""POST /ml-api/agent-activity — emit an agent activity event (best-effort).
|
|
722
|
+
|
|
723
|
+
Called on status transitions (In Progress, Backlog), every 4 files written
|
|
724
|
+
during local execution, and on errors. Never raises so a log failure can't
|
|
725
|
+
break a run.
|
|
726
|
+
"""
|
|
727
|
+
from sdk.config import BE_DEFAULT_URL
|
|
728
|
+
|
|
729
|
+
url = f"{BE_DEFAULT_URL.rstrip('/')}/ml-api/agent-activity"
|
|
730
|
+
headers = {
|
|
731
|
+
**self._headers,
|
|
732
|
+
"accept": "*/*",
|
|
733
|
+
"developer-name": "null",
|
|
734
|
+
"product": "",
|
|
735
|
+
}
|
|
736
|
+
try:
|
|
737
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
738
|
+
resp = await client.post(url, json={"activity": str(activity)}, headers=headers)
|
|
739
|
+
if resp.status_code >= 400:
|
|
740
|
+
logger.warning("client.log_activity failed status=%s body=%s",
|
|
741
|
+
resp.status_code, resp.text[:200])
|
|
742
|
+
return False
|
|
743
|
+
return True
|
|
744
|
+
except Exception as exc: # noqa: BLE001
|
|
745
|
+
logger.warning("client.log_activity error err=%s", exc)
|
|
746
|
+
return False
|
|
747
|
+
|
|
693
748
|
async def notify_whatsapp(
|
|
694
749
|
self,
|
|
695
750
|
task_id: str,
|
|
@@ -438,7 +438,10 @@ async def _code_one_repo(
|
|
|
438
438
|
# 0. Make sure the repo is present (clone from repo_url only if missing/empty).
|
|
439
439
|
await git_ops.ensure_repo(repo_path, repo_url, gh_token)
|
|
440
440
|
|
|
441
|
+
_write_count = 0
|
|
442
|
+
|
|
441
443
|
async def _on_event(event: dict) -> None:
|
|
444
|
+
nonlocal _write_count
|
|
442
445
|
if event.get("type") == "assistant":
|
|
443
446
|
for c in event.get("message", {}).get("content", []):
|
|
444
447
|
if c.get("type") == "tool_use":
|
|
@@ -450,6 +453,14 @@ async def _code_one_repo(
|
|
|
450
453
|
data={"status": "running", "current_step": label},
|
|
451
454
|
).model_dump())
|
|
452
455
|
if tname in ("Write", "Edit", "MultiEdit"):
|
|
456
|
+
_write_count += 1
|
|
457
|
+
if _write_count % 4 == 0:
|
|
458
|
+
try:
|
|
459
|
+
await client.log_activity(
|
|
460
|
+
f"Task {task_id} [{name}]: {_write_count} files written"
|
|
461
|
+
)
|
|
462
|
+
except Exception: # noqa: BLE001
|
|
463
|
+
pass
|
|
453
464
|
try:
|
|
454
465
|
from sdk import graphify
|
|
455
466
|
graphify.schedule_update(repo_path)
|
|
@@ -659,6 +670,11 @@ async def handle_pipeline_approve(
|
|
|
659
670
|
return
|
|
660
671
|
|
|
661
672
|
# ── Approved: run the local claude CLI on EACH repo the ticket names ──
|
|
673
|
+
# Mark back to In Progress — covers both direct approval in the auto-loop
|
|
674
|
+
# and a resume that arrives after the ticket sat in Backlog waiting for
|
|
675
|
+
# the human's WhatsApp reply or a usage-limit window reset.
|
|
676
|
+
await client.mark_in_progress(task_id)
|
|
677
|
+
|
|
662
678
|
# The prompt is built per-repo inside _code_one_repo so its framing matches
|
|
663
679
|
# whether that target is a git repo (branch + PR) or a plain local folder.
|
|
664
680
|
agent_mode = bool(pending.get("agent_mode", True))
|
|
@@ -765,10 +781,13 @@ async def handle_pipeline_approve(
|
|
|
765
781
|
await client.notify_whatsapp(task_id, note, summary="AgentOS local run alert")
|
|
766
782
|
except Exception: # noqa: BLE001
|
|
767
783
|
pass
|
|
784
|
+
await client.log_activity(f"Task {task_id}: local execution failed — {err[:300]}")
|
|
785
|
+
await client.move_to_backlog(task_id)
|
|
768
786
|
await send(Envelope(
|
|
769
787
|
type=Envelope.TYPE_ERROR, thread_id=thread_id,
|
|
770
788
|
data={"code": "LOCAL_EXEC_FAILED", "message": err[:300], "task_id": task_id},
|
|
771
789
|
).model_dump())
|
|
790
|
+
return
|
|
772
791
|
|
|
773
792
|
# ── Finalize on the server once per task (validation + Jira/cost) ─────
|
|
774
793
|
await send(Envelope(
|
|
@@ -786,6 +805,8 @@ async def handle_pipeline_approve(
|
|
|
786
805
|
},
|
|
787
806
|
)
|
|
788
807
|
except Exception as exc:
|
|
808
|
+
await client.log_activity(f"Task {task_id}: server finalization failed — {str(exc)[:200]}")
|
|
809
|
+
await client.move_to_backlog(task_id)
|
|
789
810
|
await send(_error_envelope(thread_id, exc).model_dump())
|
|
790
811
|
return
|
|
791
812
|
|
|
@@ -1500,9 +1521,11 @@ def _resolve_local_project_path(task: dict, payload: dict) -> str:
|
|
|
1500
1521
|
return str(Path(root).expanduser() / key)
|
|
1501
1522
|
|
|
1502
1523
|
|
|
1503
|
-
# Approval polling cadence for the first-approval gate (agentMode=true).
|
|
1504
|
-
#
|
|
1524
|
+
# Approval polling cadence for the first-approval gate (agentMode=true).
|
|
1525
|
+
# _FIRST_GATE_TIMEOUT_S: ticket moves to Backlog after 15 min with no reply.
|
|
1526
|
+
# _APPROVAL_POLL_TIMEOUT_S: longer timeout kept for mid-run crucial-decision gates.
|
|
1505
1527
|
_APPROVAL_POLL_INTERVAL_S = 15.0
|
|
1528
|
+
_FIRST_GATE_TIMEOUT_S = 900.0
|
|
1506
1529
|
_APPROVAL_POLL_TIMEOUT_S = 1200.0
|
|
1507
1530
|
# Max times a single ticket's session is resumed after a context overflow.
|
|
1508
1531
|
_MAX_CONTEXT_RESUMES = 3
|
|
@@ -1551,9 +1574,10 @@ async def _poll_approval(
|
|
|
1551
1574
|
|
|
1552
1575
|
Returns 'approved' | 'rejected' | 'stopped' | 'timeout'. The decision is the
|
|
1553
1576
|
human's WhatsApp reply, recorded backend-side under approval_log:{task_id}.
|
|
1577
|
+
Times out after _FIRST_GATE_TIMEOUT_S (15 min) so the ticket can move to Backlog.
|
|
1554
1578
|
"""
|
|
1555
1579
|
waited = 0.0
|
|
1556
|
-
while waited <
|
|
1580
|
+
while waited < _FIRST_GATE_TIMEOUT_S:
|
|
1557
1581
|
if should_stop is not None and should_stop():
|
|
1558
1582
|
return "stopped"
|
|
1559
1583
|
status = await client.approval_status(task_id)
|
|
@@ -1589,6 +1613,53 @@ async def _poll_fresh_approval(
|
|
|
1589
1613
|
return "timeout"
|
|
1590
1614
|
|
|
1591
1615
|
|
|
1616
|
+
async def _background_approval_watcher(
|
|
1617
|
+
client: BackendClient,
|
|
1618
|
+
session_id: str,
|
|
1619
|
+
task_id: str,
|
|
1620
|
+
jira_key: str,
|
|
1621
|
+
thread_id: str,
|
|
1622
|
+
send: Callable,
|
|
1623
|
+
should_stop: Callable[[], bool] | None,
|
|
1624
|
+
) -> None:
|
|
1625
|
+
"""Poll indefinitely for a late approval on a timed-out gate, then resume.
|
|
1626
|
+
|
|
1627
|
+
Spawned as a fire-and-forget task when the first-gate 15-min timeout fires.
|
|
1628
|
+
When the user approves (via WhatsApp), calls handle_pipeline_approve so the
|
|
1629
|
+
ticket resumes from exactly where it paused. handle_pipeline_approve already
|
|
1630
|
+
calls mark_in_progress, so the Jira status flips In Progress → Backlog →
|
|
1631
|
+
In Progress automatically. Cleans up _PENDING_LOCAL on any terminal outcome.
|
|
1632
|
+
"""
|
|
1633
|
+
try:
|
|
1634
|
+
while True:
|
|
1635
|
+
if should_stop is not None and should_stop():
|
|
1636
|
+
return
|
|
1637
|
+
status = await client.approval_status(task_id)
|
|
1638
|
+
if status == "approved":
|
|
1639
|
+
break
|
|
1640
|
+
if status == "rejected":
|
|
1641
|
+
await send(Envelope(
|
|
1642
|
+
type="step_update", thread_id=thread_id,
|
|
1643
|
+
data={"current_step": f"{jira_key}: rejected (late approval) — skipped"},
|
|
1644
|
+
).model_dump())
|
|
1645
|
+
return
|
|
1646
|
+
await asyncio.sleep(_APPROVAL_POLL_INTERVAL_S)
|
|
1647
|
+
# Approval arrived — move back to In Progress and resume execution.
|
|
1648
|
+
await send(Envelope(
|
|
1649
|
+
type="step_update", thread_id=thread_id,
|
|
1650
|
+
data={"current_step": f"{jira_key}: late approval received — resuming from Backlog"},
|
|
1651
|
+
).model_dump())
|
|
1652
|
+
await handle_pipeline_approve(
|
|
1653
|
+
client,
|
|
1654
|
+
{"session_id": session_id, "decision": "approved", "thread_id": thread_id},
|
|
1655
|
+
send,
|
|
1656
|
+
)
|
|
1657
|
+
except Exception as exc: # noqa: BLE001
|
|
1658
|
+
logger.warning("background_approval_watcher.failed task=%s err=%s", task_id, exc)
|
|
1659
|
+
finally:
|
|
1660
|
+
_PENDING_LOCAL.pop(session_id, None)
|
|
1661
|
+
|
|
1662
|
+
|
|
1592
1663
|
async def handle_orchestrate_auto_local_run(
|
|
1593
1664
|
client: BackendClient, payload: dict, send: Callable,
|
|
1594
1665
|
should_stop: Callable[[], bool] | None = None,
|
|
@@ -1680,6 +1751,9 @@ async def handle_orchestrate_auto_local_run(
|
|
|
1680
1751
|
attempt = 0
|
|
1681
1752
|
while True:
|
|
1682
1753
|
attempt += 1
|
|
1754
|
+
# Mark In Progress before any work begins so Jira reflects
|
|
1755
|
+
# the real state even if the run fails partway through.
|
|
1756
|
+
await client.mark_in_progress(tid)
|
|
1683
1757
|
# Phase 1 — server plans + gates. NO local code/bash runs yet.
|
|
1684
1758
|
await _run_local_pipeline(client, task_payload, send)
|
|
1685
1759
|
session_id = next(
|
|
@@ -1711,6 +1785,20 @@ async def handle_orchestrate_auto_local_run(
|
|
|
1711
1785
|
"processed": processed, "task_count": total},
|
|
1712
1786
|
).model_dump())
|
|
1713
1787
|
return
|
|
1788
|
+
if decision == "timeout":
|
|
1789
|
+
# 15 min elapsed — move to Backlog but keep _PENDING_LOCAL
|
|
1790
|
+
# alive, then spawn a background watcher that polls until
|
|
1791
|
+
# the user approves and resumes execution automatically.
|
|
1792
|
+
await client.move_to_backlog(tid)
|
|
1793
|
+
await send(Envelope(
|
|
1794
|
+
type="step_update", thread_id=thread_id,
|
|
1795
|
+
data={"current_step": f"{jira_key}: no approval in 15m — moved to Backlog"
|
|
1796
|
+
" (background watcher will auto-resume on approval)"},
|
|
1797
|
+
).model_dump())
|
|
1798
|
+
asyncio.ensure_future(_background_approval_watcher(
|
|
1799
|
+
client, session_id, tid, jira_key, thread_id, send, should_stop,
|
|
1800
|
+
))
|
|
1801
|
+
break
|
|
1714
1802
|
if decision != "approved":
|
|
1715
1803
|
_PENDING_LOCAL.pop(session_id, None)
|
|
1716
1804
|
await send(Envelope(
|
|
@@ -1756,6 +1844,8 @@ async def handle_orchestrate_auto_local_run(
|
|
|
1756
1844
|
break
|
|
1757
1845
|
except Exception as exc: # noqa: BLE001 — isolate per-ticket failures
|
|
1758
1846
|
logger.warning("orchestrate_auto.task_failed", extra={"task_id": tid, "error": str(exc)})
|
|
1847
|
+
await client.log_activity(f"Task {tid}: unexpected error — {str(exc)[:200]}")
|
|
1848
|
+
await client.move_to_backlog(tid)
|
|
1759
1849
|
await send(Envelope(
|
|
1760
1850
|
type="step_update", thread_id=thread_id,
|
|
1761
1851
|
data={"current_step": f"{jira_key or tid}: failed — {str(exc)[:160]} (continuing)"},
|
|
@@ -22,6 +22,7 @@ import json
|
|
|
22
22
|
import os
|
|
23
23
|
import re
|
|
24
24
|
import shutil
|
|
25
|
+
import sys
|
|
25
26
|
from datetime import datetime
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
from typing import Any, Awaitable, Callable
|
|
@@ -242,7 +243,6 @@ async def run_claude_local(
|
|
|
242
243
|
# destructive tool calls are blocked until a human approves (the guard reads
|
|
243
244
|
# AGENTOS_APPROVAL_DIR set below). Merges with any global/project hooks.
|
|
244
245
|
if approval_dir:
|
|
245
|
-
import sys
|
|
246
246
|
from sdk import crucial
|
|
247
247
|
settings_path = os.path.join(approval_dir, "claude_settings.json")
|
|
248
248
|
try:
|
|
@@ -271,8 +271,20 @@ async def run_claude_local(
|
|
|
271
271
|
if approval_dir:
|
|
272
272
|
env["AGENTOS_APPROVAL_DIR"] = approval_dir
|
|
273
273
|
|
|
274
|
+
# On Windows, npm/nvm install the CLI as a `.cmd`/`.bat` batch shim (e.g.
|
|
275
|
+
# claude.CMD). `create_subprocess_exec` calls CreateProcess directly — with NO
|
|
276
|
+
# command processor — and a batch shim launched that way can fail to bootstrap
|
|
277
|
+
# on some setups (notably nvm-for-Windows, where node lives behind a junction):
|
|
278
|
+
# it starts, emits nothing, and exits, surfacing here as "no result event".
|
|
279
|
+
# Route the shim through %COMSPEC% so it runs in a real cmd.exe exactly as an
|
|
280
|
+
# interactive shell would. Native `.exe` installs and POSIX paths spawn directly.
|
|
281
|
+
spawn_cmd = cmd
|
|
282
|
+
if sys.platform == "win32" and cli.lower().endswith((".cmd", ".bat")):
|
|
283
|
+
comspec = os.environ.get("COMSPEC", "cmd.exe")
|
|
284
|
+
spawn_cmd = [comspec, "/c", *cmd]
|
|
285
|
+
|
|
274
286
|
proc = await asyncio.create_subprocess_exec(
|
|
275
|
-
*
|
|
287
|
+
*spawn_cmd,
|
|
276
288
|
cwd=str(path),
|
|
277
289
|
stdin=asyncio.subprocess.PIPE,
|
|
278
290
|
stdout=asyncio.subprocess.PIPE,
|
|
@@ -183,43 +183,65 @@ def _fetch_github_token() -> str | None:
|
|
|
183
183
|
|
|
184
184
|
# ── Core update logic ─────────────────────────────────────────────────────────
|
|
185
185
|
|
|
186
|
-
def
|
|
187
|
-
"""
|
|
186
|
+
def _install_candidates() -> list[str]:
|
|
187
|
+
"""Ordered pip targets: detected URL → SSH → token-HTTPS branch → PyPI dist."""
|
|
188
|
+
cands = list(_unique([_detect_install_url(), _GITHUB_SSH]))
|
|
189
|
+
gh_token = _fetch_github_token()
|
|
190
|
+
if gh_token:
|
|
191
|
+
cands.append(f"git+https://{gh_token}@github.com/{_GITHUB_REPO}.git@{_sdk_branch()}")
|
|
192
|
+
cands.append(_DIST)
|
|
193
|
+
return cands
|
|
188
194
|
|
|
189
|
-
Tries candidates in order:
|
|
190
|
-
1. Original install URL (from direct_url.json)
|
|
191
|
-
2. GitHub SSH URL
|
|
192
|
-
3. GitHub HTTPS URL using token from /ml-api/github/token
|
|
193
|
-
4. PyPI dist name
|
|
194
|
-
Stops at the first success.
|
|
195
|
-
"""
|
|
196
|
-
flags = _pip_flags()
|
|
197
195
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
196
|
+
def _pip_install(url: str, *, force: bool = False) -> bool:
|
|
197
|
+
"""pip install --upgrade <url>. force=True adds --force-reinstall (repair)."""
|
|
198
|
+
extra = ["--force-reinstall", "--no-deps"] if force else []
|
|
199
|
+
try:
|
|
200
|
+
subprocess.run(
|
|
201
|
+
[sys.executable, "-m", "pip", "install", "--upgrade", "--quiet",
|
|
202
|
+
*extra, *_pip_flags(), url],
|
|
203
|
+
check=True, capture_output=True,
|
|
204
|
+
)
|
|
205
|
+
return True
|
|
206
|
+
except Exception: # noqa: BLE001
|
|
207
|
+
return False
|
|
208
208
|
|
|
209
|
-
# 1 + 2: detected URL and SSH fallback
|
|
210
|
-
for url in _unique([_detect_install_url(), _GITHUB_SSH]):
|
|
211
|
-
if _pip_upgrade(url):
|
|
212
|
-
return True
|
|
213
209
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
210
|
+
def _verify_install() -> bool:
|
|
211
|
+
"""True if the installed SDK imports cleanly in a FRESH process.
|
|
212
|
+
|
|
213
|
+
Guards against a partial/corrupt upgrade — on Windows the running
|
|
214
|
+
``agentos-auto.exe`` / loaded package files can't be replaced mid-pip, which
|
|
215
|
+
can leave ``sdk`` importable but missing submodules (e.g. local_exec)."""
|
|
216
|
+
try:
|
|
217
|
+
r = subprocess.run(
|
|
218
|
+
[sys.executable, "-c",
|
|
219
|
+
"import sdk, sdk.local_exec, sdk.ipc.handlers, sdk.run_auto"],
|
|
220
|
+
capture_output=True, timeout=90,
|
|
221
|
+
)
|
|
222
|
+
return r.returncode == 0
|
|
223
|
+
except Exception: # noqa: BLE001
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _repair_install() -> bool:
|
|
228
|
+
"""Force-reinstall to recover from a corrupt upgrade; verifies the result."""
|
|
229
|
+
for url in _install_candidates():
|
|
230
|
+
if _pip_install(url, force=True) and _verify_install():
|
|
219
231
|
return True
|
|
232
|
+
return False
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def self_update() -> bool:
|
|
236
|
+
"""Upgrade the installed SDK via pip. Returns True on success.
|
|
220
237
|
|
|
221
|
-
|
|
222
|
-
|
|
238
|
+
Tries candidates in order (detected URL → SSH → token-HTTPS → PyPI),
|
|
239
|
+
stopping at the first success.
|
|
240
|
+
"""
|
|
241
|
+
for url in _install_candidates():
|
|
242
|
+
if _pip_install(url):
|
|
243
|
+
return True
|
|
244
|
+
return False
|
|
223
245
|
|
|
224
246
|
|
|
225
247
|
def auto_update(quiet: bool = False) -> tuple[bool, str, str | None]:
|
|
@@ -274,6 +296,25 @@ def auto_update_and_reexec(module: str, argv: list[str]) -> None:
|
|
|
274
296
|
if os.getenv("AGENTOS_NO_UPDATE") or os.getenv("_AGENTOS_REEXEC"):
|
|
275
297
|
return
|
|
276
298
|
updated, _cur, _latest = auto_update(quiet=False)
|
|
299
|
+
|
|
300
|
+
# A self-upgrade of a RUNNING package can land broken on Windows (the active
|
|
301
|
+
# agentos-auto.exe / loaded files can't be replaced mid-pip — pip uninstalls
|
|
302
|
+
# the old then can't write the new, leaving sdk partial or removed). NEVER run
|
|
303
|
+
# on a corrupt install: verify it imports, repair (force-reinstall) if not, and
|
|
304
|
+
# abort with manual instructions only if repair also fails.
|
|
305
|
+
if not _verify_install():
|
|
306
|
+
print("⚠ the SDK looks incomplete after update — repairing …")
|
|
307
|
+
if not _repair_install():
|
|
308
|
+
print(
|
|
309
|
+
"error: the SDK self-update left the install broken and auto-repair "
|
|
310
|
+
"failed.\n reinstall it manually, then re-run agentos-auto:\n"
|
|
311
|
+
f' "{sys.executable}" -m pip install --force-reinstall '
|
|
312
|
+
f'"git+https://github.com/{_GITHUB_REPO}.git@{_sdk_branch()}"'
|
|
313
|
+
)
|
|
314
|
+
sys.exit(1)
|
|
315
|
+
print("✓ repaired.")
|
|
316
|
+
updated = True # repaired → restart on the fixed install
|
|
317
|
+
|
|
277
318
|
if not updated:
|
|
278
319
|
return
|
|
279
320
|
print("restarting on the updated version …\n")
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.17 → devops_bot_sdk-1.4.20}/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
|