devops-bot-sdk 1.4.129__tar.gz → 1.4.139__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.129 → devops_bot_sdk-1.4.139}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/client.py +74 -3
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/exceptions.py +15 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/models/responses.py +92 -8
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/api.py +33 -1
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/static/app.js +95 -34
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/static/styles.css +15 -0
- devops_bot_sdk-1.4.139/tests/test_agent_prompt_models.py +165 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/README.md +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/pyproject.toml +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/config.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/setup.cfg +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/tests/test_agents_connectors_builder.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.139
|
|
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.139" # 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
|
|
@@ -20,15 +20,15 @@ from typing import Any, AsyncGenerator
|
|
|
20
20
|
import httpx
|
|
21
21
|
|
|
22
22
|
from sdk.exceptions import (
|
|
23
|
-
BackendAuthFailed, BackendFeatureMissing,
|
|
24
|
-
TokenNotConfigured,
|
|
23
|
+
BackendAuthFailed, BackendFeatureMissing, BackendPermissionDenied, BackendUnreachable,
|
|
24
|
+
BackendVersionTooOld, TokenNotConfigured,
|
|
25
25
|
)
|
|
26
26
|
from sdk.models.envelope import Envelope
|
|
27
27
|
from sdk.models.requests import (
|
|
28
28
|
ChatRequest, OrchestrateRequest, OrchestratorRequest, WebhookPayload,
|
|
29
29
|
)
|
|
30
30
|
from sdk.models.responses import (
|
|
31
|
-
BootstrapData, ChatHistory, OAuthStarted, PipelineResult, PipelineStatus,
|
|
31
|
+
BootstrapData, ChatHistory, CustomAgent, DepartmentAgent, OAuthStarted, PipelineResult, PipelineStatus,
|
|
32
32
|
ThreadSummary, UserProfile, WebhookAck,
|
|
33
33
|
OrchestrateAutoStatusResponse, OrchestrateAutoStopResponse, OrchestrateAutoResumeResponse,
|
|
34
34
|
OrchestrateAutoStopAllResponse, OrchestrateAutoResumeAllResponse,
|
|
@@ -164,6 +164,15 @@ class BackendClient:
|
|
|
164
164
|
user = data.get("user") or {}
|
|
165
165
|
role = user.get("role") or {}
|
|
166
166
|
dept = user.get("department") or {}
|
|
167
|
+
# Department head id — the backend gates department-agent writes on
|
|
168
|
+
# `departments.head_user_id`. Accept the field under any of the shapes the
|
|
169
|
+
# ML backend might use; empty when it isn't surfaced (then headship falls
|
|
170
|
+
# back to role tier — see UserProfile.is_department_head).
|
|
171
|
+
head_id = (
|
|
172
|
+
dept.get("headUserId") or dept.get("head_user_id")
|
|
173
|
+
or (dept.get("head") or {}).get("id")
|
|
174
|
+
or user.get("departmentHeadId") or ""
|
|
175
|
+
)
|
|
167
176
|
return UserProfile(
|
|
168
177
|
user_id=str(user.get("id") or ""),
|
|
169
178
|
email=str(user.get("email") or ""),
|
|
@@ -172,6 +181,7 @@ class BackendClient:
|
|
|
172
181
|
user_type=str(user.get("userType") or "").lower(),
|
|
173
182
|
department_id=(user.get("departmentId") or dept.get("id") or None),
|
|
174
183
|
department_title=str(dept.get("title") or ""),
|
|
184
|
+
department_head_id=str(head_id or ""),
|
|
175
185
|
)
|
|
176
186
|
|
|
177
187
|
# ── Chatbot SSE ────────────────────────────────────────────────────
|
|
@@ -1217,6 +1227,19 @@ class BackendClient:
|
|
|
1217
1227
|
_check_status(resp.status_code, self._base_url)
|
|
1218
1228
|
return resp.json().get("agents", [])
|
|
1219
1229
|
|
|
1230
|
+
async def get_custom_agents(self) -> list[CustomAgent]:
|
|
1231
|
+
"""GET /api/v1/agents → parsed :class:`CustomAgent` rows.
|
|
1232
|
+
|
|
1233
|
+
The typed counterpart to :meth:`list_custom_agents` (which returns raw
|
|
1234
|
+
dicts for existing dict-based consumers). Each row is validated into a
|
|
1235
|
+
``CustomAgent`` — including the nested ``prompt`` (source / content /
|
|
1236
|
+
specialty) — so callers get attribute access and IDE completion instead of
|
|
1237
|
+
untyped dict lookups. The models are lenient (``extra=allow``), so unknown
|
|
1238
|
+
backend fields pass through rather than failing validation.
|
|
1239
|
+
"""
|
|
1240
|
+
rows = await self.list_custom_agents()
|
|
1241
|
+
return [CustomAgent(**row) for row in rows]
|
|
1242
|
+
|
|
1220
1243
|
async def update_custom_agent(self, name: str, **fields) -> dict:
|
|
1221
1244
|
"""PATCH /api/v1/agents/{name} — update fields on a custom agent."""
|
|
1222
1245
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
@@ -1261,6 +1284,54 @@ class BackendClient:
|
|
|
1261
1284
|
_check_status(resp.status_code, self._base_url)
|
|
1262
1285
|
return resp.json().get("agents", [])
|
|
1263
1286
|
|
|
1287
|
+
async def get_department_agents(self) -> list[DepartmentAgent]:
|
|
1288
|
+
"""GET /api/v1/department-agents → parsed :class:`DepartmentAgent` rows.
|
|
1289
|
+
|
|
1290
|
+
The typed counterpart to :meth:`list_department_agents` (which returns raw
|
|
1291
|
+
dicts for agentd's manifest path). Each row is validated into a
|
|
1292
|
+
``DepartmentAgent`` — including the nested ``prompt`` (source / content /
|
|
1293
|
+
specialty) — so callers get attribute access and IDE completion instead of
|
|
1294
|
+
untyped dict lookups. The models are lenient (``extra=allow``), so unknown
|
|
1295
|
+
backend fields pass through rather than failing validation.
|
|
1296
|
+
"""
|
|
1297
|
+
rows = await self.list_department_agents()
|
|
1298
|
+
return [DepartmentAgent(**row) for row in rows]
|
|
1299
|
+
|
|
1300
|
+
async def delete_department_agent(self, name: str) -> None:
|
|
1301
|
+
"""DELETE /api/v1/department-agents/{name} — remove a department default agent.
|
|
1302
|
+
|
|
1303
|
+
HEAD-GATED server-side: the backend authorises this only for the caller's
|
|
1304
|
+
department head (or ``master``) via ``rbac.scopes.is_department_head`` and
|
|
1305
|
+
returns **403** otherwise — this client does not (and cannot) re-check that
|
|
1306
|
+
locally, so a non-head caller reaching here still fails at the backend. The
|
|
1307
|
+
control panel hides Delete for non-heads (see UserProfile.is_department_head)
|
|
1308
|
+
so a 403 is the exception, not the norm. Status changes (enable/disable) are
|
|
1309
|
+
likewise head-only but not exposed here — agentd reads department agents
|
|
1310
|
+
read-only and re-derives status on restart.
|
|
1311
|
+
"""
|
|
1312
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
1313
|
+
resp = await client.delete(
|
|
1314
|
+
self._url(f"/api/v1/department-agents/{name}"),
|
|
1315
|
+
headers=self._headers,
|
|
1316
|
+
)
|
|
1317
|
+
if resp.status_code == 404:
|
|
1318
|
+
raise BackendFeatureMissing(
|
|
1319
|
+
"Department agents", self._url(f"/api/v1/department-agents/{name}"),
|
|
1320
|
+
)
|
|
1321
|
+
if resp.status_code == 403:
|
|
1322
|
+
# _check_status lets 403 through; surface it as a permission error so
|
|
1323
|
+
# the UI can say "head/manager only" rather than silently "succeeding".
|
|
1324
|
+
detail = ""
|
|
1325
|
+
try:
|
|
1326
|
+
detail = (resp.json() or {}).get("detail") or ""
|
|
1327
|
+
except Exception: # noqa: BLE001 — non-JSON body
|
|
1328
|
+
pass
|
|
1329
|
+
raise BackendPermissionDenied(
|
|
1330
|
+
detail or "Only the department head can delete a department agent.",
|
|
1331
|
+
self._url(f"/api/v1/department-agents/{name}"),
|
|
1332
|
+
)
|
|
1333
|
+
_check_status(resp.status_code, self._base_url)
|
|
1334
|
+
|
|
1264
1335
|
# NOTE: the legacy per-department "default agents" endpoints
|
|
1265
1336
|
# (/departments/{id}/default-agents[/seed|/activate|...]) were retired — that
|
|
1266
1337
|
# table never existed. Department defaults are now plain rows in the `agents`
|
|
@@ -69,6 +69,21 @@ class BackendFeatureMissing(Exception):
|
|
|
69
69
|
self.url = url
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
class BackendPermissionDenied(Exception):
|
|
73
|
+
"""Raised when the backend returns HTTP 403 — the caller is authenticated but
|
|
74
|
+
not authorised for this action (e.g. a non-head deleting a department agent,
|
|
75
|
+
gated by ``rbac.scopes.is_department_head``).
|
|
76
|
+
|
|
77
|
+
Distinct from BackendAuthFailed (401, bad/absent token): here the token is
|
|
78
|
+
valid, the action isn't permitted. The UI maps this to a "you don't have
|
|
79
|
+
permission" message rather than re-prompting for a token.
|
|
80
|
+
"""
|
|
81
|
+
def __init__(self, detail: str = "", url: str = "") -> None:
|
|
82
|
+
super().__init__(detail or f"Permission denied (HTTP 403 at {url}).")
|
|
83
|
+
self.detail = detail
|
|
84
|
+
self.url = url
|
|
85
|
+
|
|
86
|
+
|
|
72
87
|
class CollectorError(Exception):
|
|
73
88
|
"""Raised by collectors when a payload fails size or type validation."""
|
|
74
89
|
def __init__(self, message: str, path: str = "") -> None:
|
|
@@ -43,17 +43,45 @@ class UserProfile(BaseModel):
|
|
|
43
43
|
an employee may view/activate department agents but not create/edit/delete
|
|
44
44
|
them; custom agents are unrestricted for everyone.
|
|
45
45
|
"""
|
|
46
|
-
user_id:
|
|
47
|
-
email:
|
|
48
|
-
role_slug:
|
|
49
|
-
role_name:
|
|
50
|
-
user_type:
|
|
51
|
-
department_id:
|
|
52
|
-
department_title:
|
|
46
|
+
user_id: str = ""
|
|
47
|
+
email: str = ""
|
|
48
|
+
role_slug: str = ""
|
|
49
|
+
role_name: str = ""
|
|
50
|
+
user_type: str = ""
|
|
51
|
+
department_id: str | None = None
|
|
52
|
+
department_title: str = ""
|
|
53
|
+
# `departments.head_user_id` when the profile exposes it (see
|
|
54
|
+
# `is_department_head`). Empty when the backend hasn't migrated / surfaced it.
|
|
55
|
+
department_head_id: str = ""
|
|
56
|
+
|
|
57
|
+
def _role(self) -> str:
|
|
58
|
+
"""Normalised role token: lower-case, separators stripped
|
|
59
|
+
(so "team_lead"/"team-lead"/"teamlead" and "super-admin"/"superadmin"
|
|
60
|
+
all collapse to one form)."""
|
|
61
|
+
raw = (self.role_slug or self.user_type or "").lower()
|
|
62
|
+
return raw.replace("-", "").replace("_", "").replace(" ", "")
|
|
53
63
|
|
|
54
64
|
@property
|
|
55
65
|
def is_employee(self) -> bool:
|
|
56
|
-
return
|
|
66
|
+
return self._role() == "employee"
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def is_department_head(self) -> bool:
|
|
70
|
+
"""True if the caller may perform head-gated department-agent writes
|
|
71
|
+
(delete). Mirrors the backend's ``rbac.scopes.is_department_head``:
|
|
72
|
+
|
|
73
|
+
- ``master`` / ``superadmin`` / ``executive`` tiers act as managers, OR
|
|
74
|
+
- the caller's id matches the department's ``head_user_id`` (when the
|
|
75
|
+
profile provides it).
|
|
76
|
+
|
|
77
|
+
Default-deny: an unknown role with no head match resolves to False, so a
|
|
78
|
+
non-head team-lead never sees Delete (the backend also enforces this — the
|
|
79
|
+
SDK gate only hides an action that would 403 anyway). The head-id path
|
|
80
|
+
activates automatically once the backend surfaces ``head_user_id``.
|
|
81
|
+
"""
|
|
82
|
+
if self._role() in {"master", "superadmin", "executive"}:
|
|
83
|
+
return True
|
|
84
|
+
return bool(self.department_head_id) and self.department_head_id == self.user_id
|
|
57
85
|
|
|
58
86
|
|
|
59
87
|
class PipelineStatus(BaseModel):
|
|
@@ -184,3 +212,59 @@ class OrchestrateAutoStatusResponse(BaseModel):
|
|
|
184
212
|
auto_status: str = "" # discovering | running | completed | failed | no_tasks | paused
|
|
185
213
|
task_count: int = 0
|
|
186
214
|
tasks: list[OrchestrateAutoTaskItem] = []
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ── Custom / department agents ─────────────────────────────────────────
|
|
218
|
+
# Both GET /api/v1/agents (custom) and GET /api/v1/department-agents share the
|
|
219
|
+
# same backend row shape (`agents` table, projected by `_row_to_dict`); they
|
|
220
|
+
# differ only in `relation_type` ("User"/"person" vs "Department"). The two
|
|
221
|
+
# public models below extend one base so callers get a distinct, self-documenting
|
|
222
|
+
# type per endpoint without duplicating the field list.
|
|
223
|
+
|
|
224
|
+
class AgentPrompt(BaseModel):
|
|
225
|
+
"""The `prompt` object on a custom / department agent row.
|
|
226
|
+
|
|
227
|
+
Lenient (extra=allow) so any backend-side additions (e.g. new prompt
|
|
228
|
+
metadata) survive parsing without an SDK bump.
|
|
229
|
+
"""
|
|
230
|
+
model_config = {"extra": "allow"}
|
|
231
|
+
|
|
232
|
+
source: str = "" # e.g. "specialized/specialized-model-qa.md"
|
|
233
|
+
content: str = "" # the full system-prompt markdown
|
|
234
|
+
specialty: str = "" # one-line description of what the agent does
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class _AgentRow(BaseModel):
|
|
238
|
+
"""Shared shape of one `agents`-table row as the backend serialises it.
|
|
239
|
+
|
|
240
|
+
Lenient (extra=allow) so fields not modelled here (and any future ones)
|
|
241
|
+
pass through untouched. Only `status == "active"` rows are scheduled by
|
|
242
|
+
agentd; the model keeps whatever the backend sent.
|
|
243
|
+
"""
|
|
244
|
+
model_config = {"extra": "allow"}
|
|
245
|
+
|
|
246
|
+
id: str = ""
|
|
247
|
+
name: str = ""
|
|
248
|
+
media: str | None = None
|
|
249
|
+
relation_type: str = "" # "User" / "person" (custom) | "Department"
|
|
250
|
+
relation_id: str = "" # the owner id (user or department)
|
|
251
|
+
created_at: str = ""
|
|
252
|
+
updated_at: str = ""
|
|
253
|
+
status: str = "" # "active" | "inactive"
|
|
254
|
+
prompt: AgentPrompt | None = None
|
|
255
|
+
secrets: list[Any] = []
|
|
256
|
+
skills: list[Any] = []
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def is_active(self) -> bool:
|
|
260
|
+
return (self.status or "").lower() == "active"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class CustomAgent(_AgentRow):
|
|
264
|
+
"""One row from GET /api/v1/agents — a personal custom agent visible to the
|
|
265
|
+
caller under RBAC (`relation_type` is "User"/"person")."""
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class DepartmentAgent(_AgentRow):
|
|
269
|
+
"""One row from GET /api/v1/department-agents — a department's default agent
|
|
270
|
+
(`relation_type` is "Department")."""
|
|
@@ -384,11 +384,17 @@ async def ui_profile() -> dict:
|
|
|
384
384
|
return {
|
|
385
385
|
"role_slug": p.role_slug, "role_name": p.role_name,
|
|
386
386
|
"department_id": p.department_id or "", "department_title": p.department_title,
|
|
387
|
+
# is_manager (team_lead+) gates the Prompt Studio save-as-department
|
|
388
|
+
# choice AND the department-agent Edit affordance. is_department_head
|
|
389
|
+
# (manager/head — or master/executive) additionally gates Delete on a
|
|
390
|
+
# department agent, mirroring the backend's head-only write check.
|
|
387
391
|
"is_manager": not p.is_employee,
|
|
392
|
+
"is_department_head": p.is_department_head,
|
|
388
393
|
}
|
|
389
394
|
except Exception as exc: # noqa: BLE001
|
|
390
395
|
return {"role_slug": "", "role_name": "", "department_id": "",
|
|
391
|
-
"department_title": "", "is_manager": False,
|
|
396
|
+
"department_title": "", "is_manager": False,
|
|
397
|
+
"is_department_head": False, "error": str(exc)}
|
|
392
398
|
|
|
393
399
|
|
|
394
400
|
@router.get("/agents/vars", dependencies=[_auth])
|
|
@@ -975,6 +981,32 @@ async def default_agents(department_id: str = Query("")) -> dict:
|
|
|
975
981
|
return {"department_id": dept, "agents": agents, "defaults_error": ""}
|
|
976
982
|
|
|
977
983
|
|
|
984
|
+
@router.delete("/default-agents", dependencies=[_auth])
|
|
985
|
+
async def delete_default_agent(name: str = Query(...)) -> dict:
|
|
986
|
+
"""Delete a department default agent by name — HEAD/MANAGER ONLY.
|
|
987
|
+
|
|
988
|
+
Proxies DELETE /api/v1/department-agents/{name}, which the AI backend gates
|
|
989
|
+
to the department head (or master). The control panel already hides Delete for
|
|
990
|
+
non-heads (see /profile's ``is_department_head``); this maps a backend 403 to
|
|
991
|
+
HTTP 403 so a non-head who reaches here anyway gets a clear permission error
|
|
992
|
+
rather than a silent success.
|
|
993
|
+
"""
|
|
994
|
+
from sdk.client import BackendClient
|
|
995
|
+
from sdk.exceptions import BackendFeatureMissing, BackendPermissionDenied
|
|
996
|
+
|
|
997
|
+
try:
|
|
998
|
+
await BackendClient.from_config().delete_department_agent(name)
|
|
999
|
+
except BackendPermissionDenied as exc:
|
|
1000
|
+
raise HTTPException(status_code=403, detail=str(exc))
|
|
1001
|
+
except BackendFeatureMissing:
|
|
1002
|
+
raise HTTPException(
|
|
1003
|
+
status_code=404,
|
|
1004
|
+
detail=("Department agent management isn't available on this backend — "
|
|
1005
|
+
"it may be running an older version."),
|
|
1006
|
+
)
|
|
1007
|
+
return {"name": name, "deleted": True}
|
|
1008
|
+
|
|
1009
|
+
|
|
978
1010
|
# ── Dashboard reads ────────────────────────────────────────────────────────
|
|
979
1011
|
|
|
980
1012
|
@router.get("/dashboard", dependencies=[_auth])
|
|
@@ -80,6 +80,66 @@
|
|
|
80
80
|
|
|
81
81
|
window.AgentOS = { api, toast, esc };
|
|
82
82
|
|
|
83
|
+
// ── Role gates: cached profile (fetched once per page load) ────────────────
|
|
84
|
+
// is_manager: team_lead+ (may Edit a department agent). is_department_head:
|
|
85
|
+
// manager/head or master/executive (may additionally Delete one). Default-deny
|
|
86
|
+
// on error so a failed lookup hides privileged actions rather than exposing them.
|
|
87
|
+
let _profileCache = null;
|
|
88
|
+
async function getProfile() {
|
|
89
|
+
if (_profileCache) return _profileCache;
|
|
90
|
+
try { _profileCache = await api("/api/ui/profile"); }
|
|
91
|
+
catch (e) { _profileCache = { is_manager: false, is_department_head: false }; }
|
|
92
|
+
return _profileCache;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Agent prompt-shape readers ─────────────────────────────────────────────
|
|
96
|
+
// Prompt shapes differ by origin: department agents carry {content, specialty,
|
|
97
|
+
// source}; custom agents may carry {system_prompt, variables}. These read either
|
|
98
|
+
// so a card never shows blank content or a wrong variable count.
|
|
99
|
+
function agentPrompt(a) { return (a && a.prompt) || {}; }
|
|
100
|
+
function agentContent(a) { const p = agentPrompt(a); return String(p.content || p.system_prompt || "").trim(); }
|
|
101
|
+
function agentSpecialty(a) { const p = agentPrompt(a); return String(p.specialty || p.description || "").trim(); }
|
|
102
|
+
function agentVarNames(a) {
|
|
103
|
+
const vs = agentPrompt(a).variables || [];
|
|
104
|
+
return vs.map(v => (typeof v === "string" ? v : (v && v.name))).filter(Boolean);
|
|
105
|
+
}
|
|
106
|
+
function agentSummary(a) { return agentSpecialty(a) || agentContent(a).slice(0, 200); }
|
|
107
|
+
|
|
108
|
+
// One agent card: clickable name toggles an inline panel with the full prompt
|
|
109
|
+
// content. `actionsHTML` is the role-gated button row the caller supplies.
|
|
110
|
+
function agentCardHTML(a, actionsHTML) {
|
|
111
|
+
const content = agentContent(a);
|
|
112
|
+
const summary = agentSummary(a);
|
|
113
|
+
const src = agentPrompt(a).source || "";
|
|
114
|
+
const nvars = agentVarNames(a).length;
|
|
115
|
+
const nskills = (a.skills || []).length;
|
|
116
|
+
const on = a.status === "disabled" ? "off" : "on";
|
|
117
|
+
return `<div class="agent-card">
|
|
118
|
+
<div class="agent-head">
|
|
119
|
+
<button type="button" class="agent-name linklike" data-toggle-content title="Show / hide the full prompt">${esc(a.name)}</button>
|
|
120
|
+
<span class="pill ${on}">${esc(a.status || "active")}</span>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="agent-meta">${nvars} variable(s) · ${nskills} skill(s)${src ? " · <span class='muted'>" + esc(src) + "</span>" : ""}</div>
|
|
123
|
+
<div class="agent-desc">${summary ? esc(summary) : "<span class='muted'>No description provided.</span>"}</div>
|
|
124
|
+
<div class="agent-content" hidden>${content ? "<pre>" + esc(content) + "</pre>" : "<div class='muted'>No prompt content available for this agent.</div>"}</div>
|
|
125
|
+
<div class="agent-actions">${actionsHTML}</div>
|
|
126
|
+
</div>`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Wire clickable names (toggle) + Edit buttons (reveal) to their card's content
|
|
130
|
+
// panel. Call after rendering a list of agentCardHTML() cards into `container`.
|
|
131
|
+
function wireContentToggles(container) {
|
|
132
|
+
const panelOf = (el) => { const c = el.closest(".agent-card"); return c && c.querySelector(".agent-content"); };
|
|
133
|
+
container.querySelectorAll("[data-toggle-content]").forEach(b => b.onclick = () => {
|
|
134
|
+
const p = panelOf(b); if (p) p.hidden = !p.hidden;
|
|
135
|
+
});
|
|
136
|
+
// Edit = open the content view (read-only): reveal the panel and scroll to it.
|
|
137
|
+
container.querySelectorAll("[data-edit]").forEach(b => b.onclick = () => {
|
|
138
|
+
const p = panelOf(b);
|
|
139
|
+
if (p) { p.hidden = false; p.scrollIntoView({ block: "nearest" }); }
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
83
143
|
// ── Unlock gate ──────────────────────────────────────────────────────────
|
|
84
144
|
function setGateMsg(m) { const el = document.getElementById("gate-msg"); if (el) el.textContent = m || ""; }
|
|
85
145
|
function showGate(message) {
|
|
@@ -169,31 +229,38 @@
|
|
|
169
229
|
|
|
170
230
|
async function loadDefaults() {
|
|
171
231
|
try {
|
|
172
|
-
const d = await api("/api/ui/default-agents");
|
|
232
|
+
const [d, me] = [await api("/api/ui/default-agents"), await getProfile()];
|
|
173
233
|
const rows = d.agents || [];
|
|
174
234
|
if (!rows.length && d.defaults_error) {
|
|
175
235
|
$("default-list").innerHTML =
|
|
176
236
|
`<div class="empty">${esc(d.defaults_error)} Ask an admin to update the AI backend.</div>`;
|
|
177
237
|
return;
|
|
178
238
|
}
|
|
239
|
+
// Role gates (department agents): everyone Runs + views the content;
|
|
240
|
+
// team_lead+ also sees Edit (read view); only a department head/manager
|
|
241
|
+
// sees Delete (the backend enforces head-only regardless).
|
|
179
242
|
$("default-list").innerHTML = rows.length ? rows.map(a => {
|
|
180
|
-
const
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
<span class="pill ${a.status === "disabled" ? "off" : "on"}">${esc(a.status || "active")}</span>
|
|
188
|
-
</div>
|
|
189
|
-
<div class="agent-meta">${nvars} variable(s) · ${nskills} skill(s)</div>
|
|
190
|
-
<div class="agent-desc">${esc(summary)}</div>
|
|
191
|
-
<div class="agent-actions">
|
|
192
|
-
<a class="btn primary" href="/agents.html?agent=${encodeURIComponent(a.name)}">Run</a>
|
|
193
|
-
</div>
|
|
194
|
-
</div>`;
|
|
243
|
+
const enc = encodeURIComponent(a.name);
|
|
244
|
+
const actions = [
|
|
245
|
+
`<a class="btn primary" href="/agents.html?agent=${enc}">Run</a>`,
|
|
246
|
+
me.is_manager ? `<button class="btn" data-edit="${esc(a.name)}">Edit</button>` : "",
|
|
247
|
+
me.is_department_head ? `<button class="btn danger" data-del-dept="${esc(a.name)}">Delete</button>` : "",
|
|
248
|
+
].filter(Boolean).join("");
|
|
249
|
+
return agentCardHTML(a, actions);
|
|
195
250
|
}).join("")
|
|
196
251
|
: "<div class='empty'>No default agents for your department yet. They're created in the web app.</div>";
|
|
252
|
+
|
|
253
|
+
const list = $("default-list");
|
|
254
|
+
wireContentToggles(list);
|
|
255
|
+
list.querySelectorAll("[data-del-dept]").forEach(b => b.onclick = async () => {
|
|
256
|
+
const name = b.dataset.delDept;
|
|
257
|
+
if (!confirm("Delete department agent '" + name + "'? This removes it for the whole department.")) return;
|
|
258
|
+
try {
|
|
259
|
+
await api("/api/ui/default-agents?name=" + encodeURIComponent(name), { method: "DELETE" });
|
|
260
|
+
toast("Deleted " + name, "ok");
|
|
261
|
+
loadDefaults();
|
|
262
|
+
} catch (e) { toast("Delete failed: " + (e.message || ""), "err"); }
|
|
263
|
+
});
|
|
197
264
|
} catch (e) { toast("Could not load default agents", "err"); }
|
|
198
265
|
}
|
|
199
266
|
|
|
@@ -664,28 +731,22 @@
|
|
|
664
731
|
: `<option value="">(no agents yet)</option>`;
|
|
665
732
|
onAgentSelect();
|
|
666
733
|
}
|
|
734
|
+
// Custom agents: full control for everyone (view / Edit-content / enable /
|
|
735
|
+
// disable / delete) — RBAC applies only to department agents.
|
|
667
736
|
$("local-agents").innerHTML = rows.length ? rows.map(a => {
|
|
668
|
-
const
|
|
669
|
-
const
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
<div class="agent-meta">${nvars} variable(s) · ${nskills} skill(s)</div>
|
|
678
|
-
<div class="agent-desc">${esc(summary)}</div>
|
|
679
|
-
<div class="agent-actions">
|
|
680
|
-
<a class="btn primary" href="/agents.html?agent=${encodeURIComponent(a.name)}">Run</a>
|
|
681
|
-
<button class="btn" data-enable="${esc(a.name)}">Enable</button>
|
|
682
|
-
<button class="btn" data-disable="${esc(a.name)}">Disable</button>
|
|
683
|
-
<button class="btn danger" data-del="${esc(a.name)}">Delete</button>
|
|
684
|
-
</div>
|
|
685
|
-
</div>`;
|
|
737
|
+
const enc = encodeURIComponent(a.name);
|
|
738
|
+
const actions = [
|
|
739
|
+
`<a class="btn primary" href="/agents.html?agent=${enc}">Run</a>`,
|
|
740
|
+
`<button class="btn" data-edit="${esc(a.name)}">Edit</button>`,
|
|
741
|
+
`<button class="btn" data-enable="${esc(a.name)}">Enable</button>`,
|
|
742
|
+
`<button class="btn" data-disable="${esc(a.name)}">Disable</button>`,
|
|
743
|
+
`<button class="btn danger" data-del="${esc(a.name)}">Delete</button>`,
|
|
744
|
+
].join("");
|
|
745
|
+
return agentCardHTML(a, actions);
|
|
686
746
|
}).join("")
|
|
687
747
|
: "<div class='empty'>No custom agents assigned to you yet. Agents are created in the web app.</div>";
|
|
688
748
|
|
|
749
|
+
wireContentToggles($("local-agents"));
|
|
689
750
|
$("local-agents").querySelectorAll("[data-enable]").forEach(b => b.onclick = () => setStatus(b.dataset.enable, "active"));
|
|
690
751
|
$("local-agents").querySelectorAll("[data-disable]").forEach(b => b.onclick = () => setStatus(b.dataset.disable, "disabled"));
|
|
691
752
|
$("local-agents").querySelectorAll("[data-del]").forEach(b => b.onclick = async () => {
|
|
@@ -233,8 +233,23 @@ tr:last-child td { border-bottom: none; }
|
|
|
233
233
|
}
|
|
234
234
|
.agent-card .agent-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
235
235
|
.agent-name { font-weight: 700; font-size: 14px; }
|
|
236
|
+
/* Clickable agent name — reveals the full prompt below. Looks like a link, keeps
|
|
237
|
+
the bold agent-name weight; no default button chrome. */
|
|
238
|
+
.agent-name.linklike {
|
|
239
|
+
background: none; border: 0; padding: 0; margin: 0; cursor: pointer;
|
|
240
|
+
color: var(--text); text-align: left; font: inherit; font-weight: 700;
|
|
241
|
+
}
|
|
242
|
+
.agent-name.linklike:hover { color: var(--accent, #4c8bf5); text-decoration: underline; }
|
|
236
243
|
.agent-meta { font-size: 12px; color: var(--muted); }
|
|
237
244
|
.agent-desc { font-size: 13px; color: var(--text); opacity: .85; min-height: 18px; }
|
|
245
|
+
.agent-content {
|
|
246
|
+
border-top: 1px dashed var(--border); margin-top: 4px; padding-top: 8px;
|
|
247
|
+
}
|
|
248
|
+
.agent-content pre {
|
|
249
|
+
margin: 0; max-height: 340px; overflow: auto; white-space: pre-wrap;
|
|
250
|
+
word-break: break-word; font-size: 12px; line-height: 1.45;
|
|
251
|
+
color: var(--text); opacity: .92;
|
|
252
|
+
}
|
|
238
253
|
.agent-actions { display: flex; gap: 6px; margin-top: 6px; flex-wrap: wrap; }
|
|
239
254
|
|
|
240
255
|
.pill { font-size: 11px; padding: 2px 8px; border-radius: 999px; font-weight: 600; white-space: nowrap; }
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Lock-in tests for the agent-prompt request models and their Swagger shape.
|
|
2
|
+
|
|
3
|
+
These models (``sdk/ui/models.py``) intentionally MIRROR the AI backend's
|
|
4
|
+
``routes/agent_prompt.py`` request schemas field-for-field, so one JSON body
|
|
5
|
+
works against both ``/api/v1/agent-prompt/*`` (backend) and
|
|
6
|
+
``/api/ui/agent-prompt/*`` (this SDK). DBAI-101 hardens that contract: ``verify``
|
|
7
|
+
enforces a stripped ``len >= 10`` rule, ``generate``/``regenerate`` reject empty
|
|
8
|
+
required fields (all as HTTP 422), and neither the body nor any ``X-SDK-URL``
|
|
9
|
+
header may steer the SDK target.
|
|
10
|
+
|
|
11
|
+
This module pins that behaviour so it cannot silently drift out of alignment
|
|
12
|
+
with the backend. The validation tests need only ``pydantic`` (a core dep); the
|
|
13
|
+
OpenAPI/Swagger test needs the ``[ui]`` extra (``fastapi``) and is skipped when
|
|
14
|
+
it is absent.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
from pydantic import ValidationError
|
|
20
|
+
|
|
21
|
+
from sdk.ui.models import (
|
|
22
|
+
AgentPromptGenerateRequest,
|
|
23
|
+
PromptRegenerateRequest,
|
|
24
|
+
PromptVerifyRequest,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# The exact messages the backend's VerifyPrompt/Generate/Regenerate validators
|
|
28
|
+
# must also surface — a caller can rely on one message across both repos.
|
|
29
|
+
_VERIFY_MSG = "Write a system prompt first (a bit longer)."
|
|
30
|
+
_GENERATE_MSG = "agent_name, agent_type, and job are required."
|
|
31
|
+
_REGENERATE_MSG = "system_prompt is required."
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _first_error(exc: ValidationError) -> tuple[tuple, str]:
|
|
35
|
+
err = exc.errors()[0]
|
|
36
|
+
return tuple(err["loc"]), err["msg"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── verify: stripped, non-empty, len >= 10 ───────────────────────────────────
|
|
40
|
+
|
|
41
|
+
def test_verify_rejects_empty():
|
|
42
|
+
with pytest.raises(ValidationError) as ei:
|
|
43
|
+
PromptVerifyRequest(system_prompt="")
|
|
44
|
+
loc, msg = _first_error(ei.value)
|
|
45
|
+
assert loc == ("system_prompt",)
|
|
46
|
+
assert _VERIFY_MSG in msg
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_verify_rejects_short_after_strip():
|
|
50
|
+
# 9 visible chars once stripped → below the len >= 10 floor.
|
|
51
|
+
with pytest.raises(ValidationError) as ei:
|
|
52
|
+
PromptVerifyRequest(system_prompt=" shortpro ")
|
|
53
|
+
loc, msg = _first_error(ei.value)
|
|
54
|
+
assert loc == ("system_prompt",)
|
|
55
|
+
assert _VERIFY_MSG in msg
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_verify_accepts_valid_and_strips():
|
|
59
|
+
body = PromptVerifyRequest(system_prompt=" You are a helpful agent. ")
|
|
60
|
+
assert body.system_prompt == "You are a helpful agent."
|
|
61
|
+
assert body.engine == "claude_code" # shared default with the backend
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_verify_engine_is_passthrough():
|
|
65
|
+
# ``engine`` is part of the shared contract; the field is accepted verbatim.
|
|
66
|
+
assert PromptVerifyRequest(system_prompt="x" * 10, engine="server").engine == "server"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── generate: agent_name / agent_type / job all required ──────────────────────
|
|
70
|
+
|
|
71
|
+
@pytest.mark.parametrize("field", ["agent_name", "agent_type", "job"])
|
|
72
|
+
def test_generate_rejects_empty_required_field(field):
|
|
73
|
+
kwargs = {"agent_name": "Notes", "agent_type": "docs", "job": "write notes"}
|
|
74
|
+
kwargs[field] = " " # whitespace-only → empty after strip
|
|
75
|
+
with pytest.raises(ValidationError) as ei:
|
|
76
|
+
AgentPromptGenerateRequest(**kwargs)
|
|
77
|
+
loc, msg = _first_error(ei.value)
|
|
78
|
+
assert loc == (field,)
|
|
79
|
+
assert _GENERATE_MSG in msg
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_generate_accepts_valid():
|
|
83
|
+
body = AgentPromptGenerateRequest(
|
|
84
|
+
agent_name=" Release Notes ", agent_type=" documentation ", job=" summarise diffs "
|
|
85
|
+
)
|
|
86
|
+
assert body.agent_name == "Release Notes"
|
|
87
|
+
assert body.agent_type == "documentation"
|
|
88
|
+
assert body.job == "summarise diffs"
|
|
89
|
+
assert body.context == ""
|
|
90
|
+
assert body.engine == "claude_code"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# ── regenerate: system_prompt required, context fields optional ───────────────
|
|
94
|
+
|
|
95
|
+
def test_regenerate_rejects_empty_system_prompt():
|
|
96
|
+
with pytest.raises(ValidationError) as ei:
|
|
97
|
+
PromptRegenerateRequest(system_prompt=" ")
|
|
98
|
+
loc, msg = _first_error(ei.value)
|
|
99
|
+
assert loc == ("system_prompt",)
|
|
100
|
+
assert _REGENERATE_MSG in msg
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_regenerate_accepts_valid_with_optional_defaults():
|
|
104
|
+
body = PromptRegenerateRequest(system_prompt="Existing prompt to improve")
|
|
105
|
+
assert body.system_prompt == "Existing prompt to improve"
|
|
106
|
+
assert body.feedback == ""
|
|
107
|
+
assert body.agent_name == body.agent_type == body.job == ""
|
|
108
|
+
assert body.engine == "claude_code"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── field-for-field alignment guard (mirror, don't diverge) ───────────────────
|
|
112
|
+
|
|
113
|
+
def test_shared_engine_default_is_claude_code():
|
|
114
|
+
# All three shared models default engine to claude_code — the SDK always runs
|
|
115
|
+
# locally, so it accepts the field for parity and never 400s on other values.
|
|
116
|
+
assert PromptVerifyRequest.model_fields["engine"].default == "claude_code"
|
|
117
|
+
assert AgentPromptGenerateRequest.model_fields["engine"].default == "claude_code"
|
|
118
|
+
assert PromptRegenerateRequest.model_fields["engine"].default == "claude_code"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_model_fields_match_backend_contract():
|
|
122
|
+
# If the backend adds/removes a field, this catches the drift so the two
|
|
123
|
+
# repos are updated together (DBAI-101 full-stack coordination rule).
|
|
124
|
+
assert set(PromptVerifyRequest.model_fields) == {"system_prompt", "engine"}
|
|
125
|
+
assert set(AgentPromptGenerateRequest.model_fields) == {
|
|
126
|
+
"agent_name", "agent_type", "job", "context", "engine",
|
|
127
|
+
}
|
|
128
|
+
assert set(PromptRegenerateRequest.model_fields) == {
|
|
129
|
+
"system_prompt", "feedback", "agent_name", "agent_type", "job", "engine",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── Swagger / OpenAPI: no X-SDK-URL, final body shape (needs the [ui] extra) ──
|
|
134
|
+
|
|
135
|
+
def test_openapi_agent_prompt_shape_has_no_sdk_url_header():
|
|
136
|
+
pytest.importorskip("fastapi")
|
|
137
|
+
import json
|
|
138
|
+
|
|
139
|
+
from sdk.ui.server import create_app
|
|
140
|
+
|
|
141
|
+
spec = create_app().openapi()
|
|
142
|
+
|
|
143
|
+
# G-1/G-4: the target is never caller-steerable — no header anywhere.
|
|
144
|
+
blob = json.dumps(spec)
|
|
145
|
+
assert "X-SDK-URL" not in blob
|
|
146
|
+
assert "x_sdk_url" not in blob
|
|
147
|
+
|
|
148
|
+
paths = {
|
|
149
|
+
"/api/ui/agent-prompt/generate",
|
|
150
|
+
"/api/ui/agent-prompt/regenerate",
|
|
151
|
+
"/api/ui/agent-prompt/verify",
|
|
152
|
+
}
|
|
153
|
+
assert paths <= set(spec["paths"])
|
|
154
|
+
|
|
155
|
+
# None of the three routes expose a header parameter.
|
|
156
|
+
for p in paths:
|
|
157
|
+
params = spec["paths"][p]["post"].get("parameters", [])
|
|
158
|
+
assert [pr for pr in params if pr.get("in") == "header"] == []
|
|
159
|
+
|
|
160
|
+
# verify body is PromptVerifyRequest → {system_prompt, engine}.
|
|
161
|
+
verify = spec["paths"]["/api/ui/agent-prompt/verify"]["post"]
|
|
162
|
+
ref = verify["requestBody"]["content"]["application/json"]["schema"]["$ref"]
|
|
163
|
+
model = ref.rsplit("/", 1)[-1]
|
|
164
|
+
props = spec["components"]["schemas"][model]["properties"]
|
|
165
|
+
assert set(props) == {"system_prompt", "engine"}
|
|
File without changes
|
{devops_bot_sdk-1.4.129 → devops_bot_sdk-1.4.139}/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
|