devops-bot-sdk 1.4.55__tar.gz → 1.4.60__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.55 → devops_bot_sdk-1.4.60}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/pyproject.toml +1 -1
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/client.py +29 -1
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/ipc/handlers.py +139 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/README.md +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/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.60"
|
|
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.60
|
|
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.60"
|
|
34
34
|
__author__ = "AgentOS"
|
|
35
35
|
|
|
36
36
|
from sdk.client import BackendClient
|
|
@@ -58,7 +58,7 @@ def _notify_status(task_id: str, status: str) -> None:
|
|
|
58
58
|
pass
|
|
59
59
|
|
|
60
60
|
|
|
61
|
-
SDK_VERSION = "1.4.
|
|
61
|
+
SDK_VERSION = "1.4.60"
|
|
62
62
|
_POLL_INTERVAL = 3.0
|
|
63
63
|
_POLL_TIMEOUT = 600.0
|
|
64
64
|
_ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
|
|
@@ -868,6 +868,34 @@ class BackendClient:
|
|
|
868
868
|
logger.warning("client.create_task_ml error err=%s", exc)
|
|
869
869
|
return None
|
|
870
870
|
|
|
871
|
+
async def add_comment(self, task_id: str, body: str) -> bool:
|
|
872
|
+
"""POST /tasks-ml/{id}/comments — add a comment to a ticket (best-effort).
|
|
873
|
+
|
|
874
|
+
Used by the validation gate to post AI feedback (prefixed with an
|
|
875
|
+
[AI Comments] label) when an Epic/Story is rejected. JSON body: {"body": ...}.
|
|
876
|
+
Returns True on 2xx.
|
|
877
|
+
"""
|
|
878
|
+
from sdk.config import BE_DEFAULT_URL
|
|
879
|
+
|
|
880
|
+
url = f"{BE_DEFAULT_URL.rstrip('/')}/tasks-ml/{task_id}/comments"
|
|
881
|
+
headers = {
|
|
882
|
+
**self._headers, # includes Content-Type: application/json
|
|
883
|
+
"accept": "*/*",
|
|
884
|
+
"developer-name": "null",
|
|
885
|
+
"product": "",
|
|
886
|
+
}
|
|
887
|
+
try:
|
|
888
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
889
|
+
resp = await client.post(url, json={"body": str(body)}, headers=headers)
|
|
890
|
+
if resp.status_code >= 400:
|
|
891
|
+
logger.warning("client.add_comment failed task=%s status=%s body=%s",
|
|
892
|
+
task_id, resp.status_code, resp.text[:200])
|
|
893
|
+
return False
|
|
894
|
+
return True
|
|
895
|
+
except Exception as exc: # noqa: BLE001
|
|
896
|
+
logger.warning("client.add_comment error task=%s err=%s", task_id, exc)
|
|
897
|
+
return False
|
|
898
|
+
|
|
871
899
|
async def log_activity(self, activity: str) -> bool:
|
|
872
900
|
"""POST /ml-api/agent-activity — emit an agent activity event (best-effort).
|
|
873
901
|
|
|
@@ -550,6 +550,134 @@ def _epic_inherited_fields(task: dict) -> dict:
|
|
|
550
550
|
return {k: v for k, v in fields.items() if v}
|
|
551
551
|
|
|
552
552
|
|
|
553
|
+
# ── Ticket validation gate (Epics + USER-driven Stories) ────────────────────
|
|
554
|
+
# A strict quality gate: before the bot actions an Epic or a USER-created Story it
|
|
555
|
+
# checks the ticket meets the required standard. On failure it posts a harsh,
|
|
556
|
+
# technical comment (prefixed with the label below) and moves the ticket to
|
|
557
|
+
# Backlog. SYSTEM-created Stories (spun off an Epic by the bot) are trusted and
|
|
558
|
+
# skip validation. A later run re-validates; once the gaps are addressed in the
|
|
559
|
+
# description / Human Instructions, it passes and the normal flow proceeds.
|
|
560
|
+
_AI_COMMENT_LABEL = "[AI Comments]"
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _build_validation_prompt(kind: str, jira_key: str, summary: str,
|
|
564
|
+
description: str | None, human: str | None) -> str:
|
|
565
|
+
k = kind.upper()
|
|
566
|
+
parts: list[str] = [
|
|
567
|
+
f"You are a STRICT ticket-quality gate for a JIRA {k}. Decide whether this "
|
|
568
|
+
"ticket meets the required standard to be actioned autonomously. Be HARSH: if "
|
|
569
|
+
"the required structure or information is not strictly present, REJECT it — a "
|
|
570
|
+
"vague or under-specified ticket must NOT pass.",
|
|
571
|
+
f"=== {k} {jira_key} ===\nSummary: {summary or '(none)'}",
|
|
572
|
+
"Description:\n" + (_clip_text(description.strip()) if description and description.strip() else "(none provided)"),
|
|
573
|
+
"Human Instructions:\n" + (human.strip() if human and human.strip() else "(none provided)"),
|
|
574
|
+
]
|
|
575
|
+
parts.append(
|
|
576
|
+
"Judge the ticket in the context of what it ACTUALLY asks for — it may be "
|
|
577
|
+
"software, documentation, analysis, Finance, HR or operations work. Do NOT "
|
|
578
|
+
"impose software requirements on a non-software ticket.\n\n"
|
|
579
|
+
"A VALID ticket MUST have:\n"
|
|
580
|
+
"- A clear, unambiguous goal and scope (no hand-waving).\n"
|
|
581
|
+
"- A clearly-defined deliverable or outcome, and — when a location matters — "
|
|
582
|
+
"WHERE to produce it (a folder path, a repository, or a document).\n"
|
|
583
|
+
"- Observable, testable acceptance criteria (or an equally concrete definition of done).\n"
|
|
584
|
+
"- ONLY IF the work requires CODE CHANGES to a software project: the target "
|
|
585
|
+
"repository link(s) and/or file path(s), the branch/PR convention, and "
|
|
586
|
+
"build/run/test steps. Do NOT demand these for documentation, content, analysis "
|
|
587
|
+
"or other non-code deliverables.\n"
|
|
588
|
+
+ ("- Enough concrete detail to decompose into independent user stories.\n"
|
|
589
|
+
if kind == "epic" else
|
|
590
|
+
"- A clear 'As a <role>, I want <capability>, so that <benefit>' intent plus "
|
|
591
|
+
"testable acceptance criteria.\n")
|
|
592
|
+
+ "- Any THIRD-PARTY INTEGRATION or human-effort dependency the work GENUINELY "
|
|
593
|
+
"needs (API keys, OAuth credentials, external accounts, secrets, manual "
|
|
594
|
+
"provisioning, DNS, billing) MUST be named WITH who provides it; if such a "
|
|
595
|
+
"REQUIRED dependency is undefined, REJECT.\n\n"
|
|
596
|
+
"A ticket that clearly states its deliverable and where to produce it is VALID "
|
|
597
|
+
"even without repository/branch details. Reject only for genuinely missing or "
|
|
598
|
+
"ambiguous information — not for absent software details the task does not need.\n\n"
|
|
599
|
+
"Output ONLY a fenced ```json block, nothing else:\n"
|
|
600
|
+
"{\n"
|
|
601
|
+
' "valid": true or false,\n'
|
|
602
|
+
' "comment": "When invalid: a concise, TECHNICAL rejection that lists EXACTLY '
|
|
603
|
+
"what is missing or ambiguous and must be added, in terms appropriate to the "
|
|
604
|
+
"ticket's domain (for software: API contract, data model/schema, auth flow, "
|
|
605
|
+
"env/config, target repository/branch; for other work: the missing deliverable, "
|
|
606
|
+
'location, criteria, or dependency). When valid: empty string."\n'
|
|
607
|
+
"}"
|
|
608
|
+
)
|
|
609
|
+
return "\n\n".join(parts)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _parse_validation(text: str | None) -> tuple[bool, str]:
|
|
613
|
+
"""(valid, comment) from the validator's JSON output. Defaults to invalid."""
|
|
614
|
+
obj = _extract_json_block(text) or {}
|
|
615
|
+
return bool(obj.get("valid")), str(obj.get("comment") or "").strip()
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
async def _validate_ticket_or_reject(
|
|
619
|
+
client: BackendClient, task: dict, kind: str, payload: dict,
|
|
620
|
+
send: Callable, thread_id: str,
|
|
621
|
+
) -> bool:
|
|
622
|
+
"""Strict gate. Returns True to proceed, False if the ticket was rejected
|
|
623
|
+
(comment posted + moved to Backlog). Fails OPEN if the validator can't run —
|
|
624
|
+
we never block a ticket on our own infrastructure error."""
|
|
625
|
+
from sdk import local_exec
|
|
626
|
+
|
|
627
|
+
tid = str(task.get("id", ""))
|
|
628
|
+
jira_key = task.get("jiraIssueKey") or task.get("jiraKey", "") or kind.upper()
|
|
629
|
+
project_path = _resolve_local_project_path(task, payload)
|
|
630
|
+
|
|
631
|
+
await send(Envelope(
|
|
632
|
+
type="step_update", thread_id=thread_id,
|
|
633
|
+
data={"jira_task_id": tid, "task_id": tid,
|
|
634
|
+
"current_step": f"{kind.title()} {jira_key}: validating ticket against the standard"},
|
|
635
|
+
).model_dump())
|
|
636
|
+
|
|
637
|
+
res = await local_exec.run_claude_local(
|
|
638
|
+
_build_validation_prompt(kind, jira_key, task.get("summary", "") or "",
|
|
639
|
+
task.get("description"), task.get("humanInstructions")),
|
|
640
|
+
project_path, allowed_tools=_RECON_ALLOWED_TOOLS, # read-only judgment
|
|
641
|
+
github_token=await client.github_token(),
|
|
642
|
+
)
|
|
643
|
+
if not res.get("ok"):
|
|
644
|
+
await client.log_activity(
|
|
645
|
+
f"{kind} {jira_key}: validation could not run ({(res.get('error') or '')[:120]}) — proceeding"
|
|
646
|
+
)
|
|
647
|
+
return True # fail-open on validator infra error
|
|
648
|
+
|
|
649
|
+
valid, comment = _parse_validation(res.get("result"))
|
|
650
|
+
if valid:
|
|
651
|
+
await send(Envelope(
|
|
652
|
+
type="step_update", thread_id=thread_id,
|
|
653
|
+
data={"jira_task_id": tid, "current_step": f"{kind.title()} {jira_key}: validation passed"},
|
|
654
|
+
).model_dump())
|
|
655
|
+
return True
|
|
656
|
+
|
|
657
|
+
# Rejected — post a labelled, technical comment and move to Backlog.
|
|
658
|
+
fallback = ("This ticket does not meet the required standard. Provide: a clear goal "
|
|
659
|
+
"and scope; explicit Human Instructions (target repository/branch and/or "
|
|
660
|
+
"file paths, PR convention, build/run/test steps); testable acceptance "
|
|
661
|
+
"criteria; and the credentials/ownership for any third-party integration.")
|
|
662
|
+
body = f"{_AI_COMMENT_LABEL}\n\n{comment or fallback}"
|
|
663
|
+
await client.add_comment(tid, body)
|
|
664
|
+
# The backend only accepts a Backlog transition when a timer is running, so start
|
|
665
|
+
# one (mark In Progress) then park it — otherwise move_to_backlog 404s with
|
|
666
|
+
# "No running timer found". This In-Progress blip is the reject/park path only.
|
|
667
|
+
try:
|
|
668
|
+
await client.mark_in_progress(tid)
|
|
669
|
+
except Exception: # noqa: BLE001
|
|
670
|
+
pass
|
|
671
|
+
await client.move_to_backlog(tid)
|
|
672
|
+
await client.log_activity(f"{kind} {jira_key}: REJECTED by validation gate — commented + moved to Backlog")
|
|
673
|
+
await send(Envelope(
|
|
674
|
+
type=Envelope.TYPE_ERROR, thread_id=thread_id,
|
|
675
|
+
data={"code": f"{kind.upper()}_VALIDATION_REJECTED", "task_id": tid,
|
|
676
|
+
"message": (comment or fallback)[:300]},
|
|
677
|
+
).model_dump())
|
|
678
|
+
return False
|
|
679
|
+
|
|
680
|
+
|
|
553
681
|
async def _handle_epic(
|
|
554
682
|
client: BackendClient, task: dict, payload: dict, send: Callable, thread_id: str,
|
|
555
683
|
) -> None:
|
|
@@ -628,6 +756,7 @@ async def _handle_epic(
|
|
|
628
756
|
**inherited,
|
|
629
757
|
"type": "Story", # Epic decomposes into user Stories; a Story later
|
|
630
758
|
# becomes a Task (see _handle_story) when it is run.
|
|
759
|
+
"drivenBy": "SYSTEM", # bot-created → trusted, skips the validation gate
|
|
631
760
|
"status": "To Do",
|
|
632
761
|
"summary": str(st.get("summary", "")).strip()[:250],
|
|
633
762
|
"description": str(st.get("description", "") or "")[:20000],
|
|
@@ -2720,7 +2849,17 @@ async def handle_orchestrate_auto_local_run(
|
|
|
2720
2849
|
if ttype in ("epic", "story"):
|
|
2721
2850
|
handler = _handle_epic if ttype == "epic" else _handle_story
|
|
2722
2851
|
code = "EPIC_DECOMPOSE_FAILED" if ttype == "epic" else "STORY_HANDLE_FAILED"
|
|
2852
|
+
# Validation gate: Epics are always validated; a Story only when it
|
|
2853
|
+
# is USER-driven (a SYSTEM/bot-created Story is trusted). A rejected
|
|
2854
|
+
# ticket is commented + moved to Backlog inside the gate — skip it.
|
|
2855
|
+
driven_by = str(task.get("drivenBy") or "").strip().upper()
|
|
2856
|
+
needs_validation = (ttype == "epic") or (ttype == "story" and driven_by != "SYSTEM")
|
|
2723
2857
|
try:
|
|
2858
|
+
if needs_validation and not await _validate_ticket_or_reject(
|
|
2859
|
+
client, task, ttype, payload, send, thread_id
|
|
2860
|
+
):
|
|
2861
|
+
processed += 1
|
|
2862
|
+
continue
|
|
2724
2863
|
await handler(client, task, payload, send, thread_id)
|
|
2725
2864
|
except Exception as exc: # noqa: BLE001
|
|
2726
2865
|
logger.warning("auto_local.%s_failed task=%s err=%s", ttype, tid, exc)
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.55 → devops_bot_sdk-1.4.60}/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
|