devops-bot-sdk 1.4.1__tar.gz → 1.4.3__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.1 → devops_bot_sdk-1.4.3}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/client.py +1 -1
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/git_ops.py +95 -5
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/ipc/handlers.py +8 -3
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/local_exec.py +16 -1
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/README.md +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.1 → devops_bot_sdk-1.4.3}/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.3"
|
|
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.3
|
|
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.3"
|
|
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.3"
|
|
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
|
|
@@ -9,11 +9,14 @@ names). Everything here is best-effort and never raises into the run.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
|
+
import logging
|
|
12
13
|
import os
|
|
13
14
|
import re
|
|
14
15
|
import shutil
|
|
15
16
|
from pathlib import Path
|
|
16
17
|
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
17
20
|
|
|
18
21
|
def slugify(text: str | None, max_len: int = 40) -> str:
|
|
19
22
|
s = re.sub(r"[^a-z0-9]+", "-", (text or "").lower()).strip("-")
|
|
@@ -92,13 +95,72 @@ async def start_branch(
|
|
|
92
95
|
return {"branch": branch, "base": base}
|
|
93
96
|
|
|
94
97
|
|
|
98
|
+
async def _remote_owner_repo(path: str, env: dict) -> tuple[str, str] | None:
|
|
99
|
+
"""Parse origin's URL into (owner, repo). Handles https and ssh forms."""
|
|
100
|
+
rc, out, _ = await _run(["git", "remote", "get-url", "origin"], path, env)
|
|
101
|
+
if rc != 0 or not out:
|
|
102
|
+
return None
|
|
103
|
+
m = re.search(r"github\.com[:/]+([^/]+)/(.+?)(?:\.git)?/?$", out.strip())
|
|
104
|
+
if not m:
|
|
105
|
+
return None
|
|
106
|
+
return m.group(1), m.group(2)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
async def _create_pr_via_api(
|
|
110
|
+
owner: str, repo: str, base: str, head: str, title: str, body: str, token: str,
|
|
111
|
+
) -> str | None:
|
|
112
|
+
"""Open a PR through the GitHub REST API (no `gh` CLI needed).
|
|
113
|
+
|
|
114
|
+
The token from /ml-api/github/token carries `repo` scope, so it can both push
|
|
115
|
+
and open PRs. Returns the PR html_url, or the existing open PR's url when one
|
|
116
|
+
already exists for this head (422). Returns None (and logs) on any other error.
|
|
117
|
+
"""
|
|
118
|
+
import httpx
|
|
119
|
+
|
|
120
|
+
api = f"https://api.github.com/repos/{owner}/{repo}/pulls"
|
|
121
|
+
headers = {
|
|
122
|
+
"Authorization": f"token {token}",
|
|
123
|
+
"Accept": "application/vnd.github+json",
|
|
124
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
125
|
+
}
|
|
126
|
+
try:
|
|
127
|
+
async with httpx.AsyncClient(timeout=30.0) as c:
|
|
128
|
+
resp = await c.post(
|
|
129
|
+
api, headers=headers,
|
|
130
|
+
json={"title": title, "head": head, "base": base, "body": body},
|
|
131
|
+
)
|
|
132
|
+
if resp.status_code == 201:
|
|
133
|
+
return resp.json().get("html_url")
|
|
134
|
+
# 422 = a PR already exists for this head (or validation issue). Try to
|
|
135
|
+
# surface the existing open PR rather than reporting "no PR".
|
|
136
|
+
if resp.status_code == 422:
|
|
137
|
+
async with httpx.AsyncClient(timeout=30.0) as c:
|
|
138
|
+
r2 = await c.get(
|
|
139
|
+
api, headers=headers,
|
|
140
|
+
params={"head": f"{owner}:{head}", "state": "open"},
|
|
141
|
+
)
|
|
142
|
+
if r2.status_code == 200 and r2.json():
|
|
143
|
+
return r2.json()[0].get("html_url")
|
|
144
|
+
logger.warning(
|
|
145
|
+
"git_ops.pr_api_failed owner=%s repo=%s head=%s status=%s body=%s",
|
|
146
|
+
owner, repo, head, resp.status_code, resp.text[:300],
|
|
147
|
+
)
|
|
148
|
+
except Exception as exc: # noqa: BLE001
|
|
149
|
+
logger.warning("git_ops.pr_api_error head=%s error=%s", head, exc)
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
95
153
|
async def finish_pr(
|
|
96
154
|
project_path: str, branch: str, base: str, title: str, body: str,
|
|
97
155
|
github_token: str | None,
|
|
98
156
|
) -> dict:
|
|
99
157
|
"""Stage all, commit, push the branch, open a PR. Best-effort.
|
|
100
158
|
|
|
101
|
-
|
|
159
|
+
PR creation prefers the GitHub REST API (works with just the token — no `gh`
|
|
160
|
+
install required) and falls back to the `gh` CLI. Every failure path is
|
|
161
|
+
logged so a missing PR is diagnosable instead of silent.
|
|
162
|
+
|
|
163
|
+
Returns {"pushed": bool, "pr_url": str | None, "pr_error": str | None}.
|
|
102
164
|
"""
|
|
103
165
|
path = str(Path(project_path).expanduser())
|
|
104
166
|
env = _git_env(github_token)
|
|
@@ -107,18 +169,46 @@ async def finish_pr(
|
|
|
107
169
|
# commit — no-op (non-zero) when there's nothing staged; ignore that case.
|
|
108
170
|
await _run(["git", "commit", "-m", title], path, env)
|
|
109
171
|
|
|
110
|
-
rc, _,
|
|
172
|
+
rc, _, push_err = await _run(
|
|
111
173
|
["git", "push", "-u", "origin", branch, "--force-with-lease"],
|
|
112
174
|
path, env, timeout=300.0,
|
|
113
175
|
)
|
|
114
176
|
pushed = rc == 0
|
|
177
|
+
if not pushed:
|
|
178
|
+
logger.warning("git_ops.push_failed branch=%s error=%s", branch, push_err[:300])
|
|
179
|
+
return {"pushed": False, "pr_url": None, "pr_error": f"push failed: {push_err[:200]}"}
|
|
180
|
+
|
|
115
181
|
pr_url: str | None = None
|
|
116
|
-
|
|
117
|
-
|
|
182
|
+
pr_error: str | None = None
|
|
183
|
+
|
|
184
|
+
# 1. Preferred: REST API (no `gh` dependency).
|
|
185
|
+
owner_repo = await _remote_owner_repo(path, env)
|
|
186
|
+
if github_token and owner_repo:
|
|
187
|
+
owner, repo = owner_repo
|
|
188
|
+
pr_url = await _create_pr_via_api(owner, repo, base, branch, title, body, github_token)
|
|
189
|
+
|
|
190
|
+
# 2. Fallback: gh CLI, if installed.
|
|
191
|
+
if not pr_url and shutil.which("gh"):
|
|
192
|
+
rc, out, gh_err = await _run(
|
|
118
193
|
["gh", "pr", "create", "--base", base, "--head", branch,
|
|
119
194
|
"--title", title, "--body", body],
|
|
120
195
|
path, env, timeout=120.0,
|
|
121
196
|
)
|
|
122
197
|
if rc == 0 and out:
|
|
123
198
|
pr_url = out.strip().splitlines()[-1]
|
|
124
|
-
|
|
199
|
+
else:
|
|
200
|
+
pr_error = f"gh pr create failed: {gh_err[:200]}"
|
|
201
|
+
logger.warning("git_ops.gh_pr_failed branch=%s error=%s", branch, gh_err[:300])
|
|
202
|
+
|
|
203
|
+
if not pr_url and pr_error is None:
|
|
204
|
+
# Pushed, but no PR and no specific error captured above.
|
|
205
|
+
pr_error = (
|
|
206
|
+
"no PR created — REST API returned no URL"
|
|
207
|
+
if (github_token and owner_repo)
|
|
208
|
+
else "no PR created — missing GitHub token or unrecognized origin remote"
|
|
209
|
+
if not shutil.which("gh")
|
|
210
|
+
else "no PR created"
|
|
211
|
+
)
|
|
212
|
+
logger.warning("git_ops.no_pr branch=%s reason=%s", branch, pr_error)
|
|
213
|
+
|
|
214
|
+
return {"pushed": pushed, "pr_url": pr_url, "pr_error": pr_error}
|
|
@@ -521,15 +521,20 @@ async def handle_pipeline_approve(
|
|
|
521
521
|
body=(result.get("result") or "")[:4000],
|
|
522
522
|
github_token=gh_token,
|
|
523
523
|
)
|
|
524
|
-
|
|
524
|
+
if pr.get("pushed"):
|
|
525
|
+
step = f"Pushed {branch_info['branch']}"
|
|
526
|
+
else:
|
|
527
|
+
step = f"Push failed for {branch_info['branch']}"
|
|
525
528
|
if pr.get("pr_url"):
|
|
526
529
|
step += f" · PR: {pr['pr_url']}"
|
|
530
|
+
elif pr.get("pr_error"):
|
|
531
|
+
step += f" · PR not created: {pr['pr_error']}"
|
|
527
532
|
await send(Envelope(
|
|
528
533
|
type="step_update", thread_id=thread_id,
|
|
529
534
|
data={"status": "running", "current_step": step},
|
|
530
535
|
).model_dump())
|
|
531
|
-
except Exception: # noqa: BLE001
|
|
532
|
-
|
|
536
|
+
except Exception as exc: # noqa: BLE001
|
|
537
|
+
logger.warning("local_run.finish_pr_failed", error=str(exc))
|
|
533
538
|
|
|
534
539
|
# ── Notify the user on WhatsApp for a hard error or context overflow ──
|
|
535
540
|
# The backend forwards `message` to WhatsApp (POST /approvals-ml). Best-effort.
|
|
@@ -32,6 +32,14 @@ DEFAULT_ALLOWED_TOOLS = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"]
|
|
|
32
32
|
# Hard ceiling so a runaway agent can't hang the sidecar forever.
|
|
33
33
|
_DEFAULT_TIMEOUT_S = 1800.0
|
|
34
34
|
|
|
35
|
+
# StreamReader buffer for the claude CLI's stdout. `--output-format stream-json`
|
|
36
|
+
# emits one JSON object per line, and a single event can embed a whole file's
|
|
37
|
+
# contents (e.g. a Read tool_use_result), easily exceeding asyncio's default
|
|
38
|
+
# 64 KB line limit — which makes readline() raise LimitOverrunError ("Separator
|
|
39
|
+
# is found, but chunk is longer than limit"). 64 MB comfortably covers large
|
|
40
|
+
# file reads/diffs in one event.
|
|
41
|
+
_STREAM_LIMIT_BYTES = 64 * 1024 * 1024
|
|
42
|
+
|
|
35
43
|
# Markers that indicate the model ran out of context window.
|
|
36
44
|
_CONTEXT_MARKERS = (
|
|
37
45
|
"context window", "context length", "maximum context", "too many tokens",
|
|
@@ -135,6 +143,7 @@ async def run_claude_local(
|
|
|
135
143
|
stdout=asyncio.subprocess.PIPE,
|
|
136
144
|
stderr=asyncio.subprocess.PIPE,
|
|
137
145
|
env=env,
|
|
146
|
+
limit=_STREAM_LIMIT_BYTES,
|
|
138
147
|
)
|
|
139
148
|
|
|
140
149
|
result_event: dict | None = None
|
|
@@ -145,7 +154,13 @@ async def run_claude_local(
|
|
|
145
154
|
nonlocal result_event, session_id
|
|
146
155
|
assert proc.stdout is not None
|
|
147
156
|
while True:
|
|
148
|
-
|
|
157
|
+
try:
|
|
158
|
+
raw = await proc.stdout.readline()
|
|
159
|
+
except ValueError:
|
|
160
|
+
# Line exceeded even _STREAM_LIMIT_BYTES. readline() has already
|
|
161
|
+
# advanced past the oversized chunk, so we can safely skip this
|
|
162
|
+
# one event and keep streaming the rest rather than aborting.
|
|
163
|
+
continue
|
|
149
164
|
if not raw:
|
|
150
165
|
break
|
|
151
166
|
line = raw.decode("utf-8", errors="replace").strip()
|
|
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
|