devops-bot-sdk 1.4.119__tar.gz → 1.4.124__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.119 → devops_bot_sdk-1.4.124}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/inventory.py +45 -33
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/config.py +69 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/api.py +133 -52
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/ip_allowlist.py +16 -10
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/static/app.js +21 -7
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/README.md +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/pyproject.toml +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/client.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/setup.cfg +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/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.124
|
|
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.124" # 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
|
|
@@ -16,22 +16,39 @@ import subprocess
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
18
|
from sdk.agents import usage as usage_mod
|
|
19
|
-
from sdk.agents.manifest import
|
|
20
|
-
from sdk.agents.prompt_vars import extract_variables
|
|
19
|
+
from sdk.agents.manifest import load_skills
|
|
21
20
|
|
|
22
21
|
logger = logging.getLogger("sdk.agents.inventory")
|
|
23
22
|
|
|
24
23
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
24
|
+
def _map_backend_agent(row: dict, source: str) -> dict:
|
|
25
|
+
"""Project a backend prompt-based ``agents`` row into the inventory shape."""
|
|
26
|
+
prompt = row.get("prompt") or {}
|
|
27
|
+
sys_prompt = prompt.get("system_prompt", "") if isinstance(prompt, dict) else ""
|
|
28
|
+
pvars = prompt.get("variables", []) if isinstance(prompt, dict) else []
|
|
29
|
+
return {
|
|
30
|
+
"name": row.get("name"),
|
|
31
|
+
"description": sys_prompt[:200],
|
|
32
|
+
"variables": [v.get("name") for v in pvars if isinstance(v, dict) and v.get("name")],
|
|
33
|
+
"skills": row.get("skills", []),
|
|
34
|
+
"status": row.get("status", "active"),
|
|
35
|
+
"source": source,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async def _custom_agents() -> list[dict]:
|
|
40
|
+
"""The caller's custom agents (``relation_type=person``) from the backend.
|
|
41
|
+
|
|
42
|
+
Backend is the source of truth — agents are authored in the web app and read
|
|
43
|
+
via GET /api/v1/agents. Best-effort: degrades to empty on any error.
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
from sdk.client import BackendClient
|
|
47
|
+
rows = await BackendClient.from_config().list_custom_agents()
|
|
48
|
+
return [_map_backend_agent(r, "custom") for r in rows]
|
|
49
|
+
except Exception as exc: # noqa: BLE001
|
|
50
|
+
logger.debug("custom-agent inventory failed: %s", exc)
|
|
51
|
+
return []
|
|
35
52
|
|
|
36
53
|
|
|
37
54
|
def _skills() -> list[dict]:
|
|
@@ -101,31 +118,26 @@ def _claude_subagents() -> list[dict]:
|
|
|
101
118
|
|
|
102
119
|
|
|
103
120
|
async def _default_agents() -> list[dict]:
|
|
104
|
-
"""Department default agents
|
|
121
|
+
"""Department default agents (``relation_type=department`` + org-wide common)
|
|
122
|
+
from the backend (best-effort).
|
|
105
123
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
department's templates locally — no AI-backend ``/departments/{id}/
|
|
109
|
-
default-agents`` call.
|
|
124
|
+
Reads the live rows via GET /api/v1/department-agents — the source of truth —
|
|
125
|
+
rather than the bundled SDK library. Degrades to empty on any error.
|
|
110
126
|
"""
|
|
111
127
|
try:
|
|
112
128
|
from sdk.client import BackendClient
|
|
113
|
-
from sdk.agents.library import (
|
|
114
|
-
load_common_templates, load_department, resolve_department_slug,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
129
|
client = BackendClient.from_config()
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
try:
|
|
131
|
+
title = (await client.profile()).department_title or ""
|
|
132
|
+
except Exception: # noqa: BLE001 — department label is cosmetic
|
|
133
|
+
title = ""
|
|
134
|
+
rows = await client.list_department_agents()
|
|
135
|
+
out = []
|
|
136
|
+
for r in rows:
|
|
137
|
+
entry = _map_backend_agent(r, "default")
|
|
138
|
+
entry["department"] = title
|
|
139
|
+
out.append(entry)
|
|
140
|
+
return out
|
|
129
141
|
except Exception as exc: # noqa: BLE001
|
|
130
142
|
logger.debug("default-agent inventory failed: %s", exc)
|
|
131
143
|
return []
|
|
@@ -133,7 +145,7 @@ async def _default_agents() -> list[dict]:
|
|
|
133
145
|
|
|
134
146
|
async def build_inventory() -> dict:
|
|
135
147
|
"""The full snapshot: agents, subagents, tools, plugins + per-agent usage."""
|
|
136
|
-
customs = _custom_agents()
|
|
148
|
+
customs = await _custom_agents()
|
|
137
149
|
usage = usage_mod.summary()
|
|
138
150
|
# Attach utilisation to each custom agent.
|
|
139
151
|
for a in customs:
|
|
@@ -190,6 +190,75 @@ def get_allowed_ips() -> list[str]:
|
|
|
190
190
|
return out
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
class PermanentAllowlistEntry(ValueError):
|
|
194
|
+
"""Raised when trying to disable an IP that is permanently whitelisted via ``.env``."""
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def permanent_allowed_ips() -> list[str]:
|
|
198
|
+
"""IPs/CIDRs whitelisted **permanently** — from ``.env`` (``~/.agentos/.env`` or
|
|
199
|
+
CWD) or the ``AGENTOS_UI_ALLOWED_IPS`` process env var.
|
|
200
|
+
|
|
201
|
+
These are the fixed baseline: they are ALWAYS allowed and **cannot be disabled**
|
|
202
|
+
through the API — change them by editing ``.env`` and restarting.
|
|
203
|
+
"""
|
|
204
|
+
out: list[str] = []
|
|
205
|
+
for src in (_dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
|
|
206
|
+
os.environ.get("AGENTOS_UI_ALLOWED_IPS", "")):
|
|
207
|
+
for ip in _parse_ip_list(src):
|
|
208
|
+
if ip not in out:
|
|
209
|
+
out.append(ip)
|
|
210
|
+
return out
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def runtime_allowed_ips() -> list[str]:
|
|
214
|
+
"""IPs/CIDRs whitelisted **at runtime** (config.toml ``allowed_ips``) — added by
|
|
215
|
+
the whitelist endpoint and removable by the disable endpoint. They stay in the
|
|
216
|
+
allowlist until disabled."""
|
|
217
|
+
return _parse_ip_list(_load_raw().get("allowed_ips", ""))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def add_allowed_ip(ip: str) -> list[str]:
|
|
221
|
+
"""Whitelist an IP/CIDR at RUNTIME (persisted to config.toml ``allowed_ips``).
|
|
222
|
+
|
|
223
|
+
Idempotent. The middleware re-reads the allowlist per request, so the IP is
|
|
224
|
+
honoured immediately (no restart) and stays until disabled. An IP already
|
|
225
|
+
permanently whitelisted via ``.env`` is left as-is (no runtime duplicate).
|
|
226
|
+
Returns the merged allowlist.
|
|
227
|
+
"""
|
|
228
|
+
ip = (ip or "").strip()
|
|
229
|
+
if not ip:
|
|
230
|
+
raise ValueError("ip is required")
|
|
231
|
+
if ip in permanent_allowed_ips(): # already permanent — nothing to add
|
|
232
|
+
return get_allowed_ips()
|
|
233
|
+
raw = _load_raw() # preserves token_enc / base_url / etc.
|
|
234
|
+
entries = _parse_ip_list(raw.get("allowed_ips", ""))
|
|
235
|
+
if ip not in entries:
|
|
236
|
+
entries.append(ip)
|
|
237
|
+
raw["allowed_ips"] = ", ".join(entries)
|
|
238
|
+
_save_raw(raw)
|
|
239
|
+
return get_allowed_ips()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def remove_allowed_ip(ip: str) -> list[str]:
|
|
243
|
+
"""Disable a RUNTIME-whitelisted IP/CIDR (remove from config.toml ``allowed_ips``).
|
|
244
|
+
|
|
245
|
+
Raises :class:`PermanentAllowlistEntry` if the IP is permanently whitelisted via
|
|
246
|
+
``.env`` — those are never removable through the API (edit ``.env`` instead).
|
|
247
|
+
Returns the merged allowlist.
|
|
248
|
+
"""
|
|
249
|
+
ip = (ip or "").strip()
|
|
250
|
+
if ip in permanent_allowed_ips():
|
|
251
|
+
raise PermanentAllowlistEntry(
|
|
252
|
+
f"{ip} is permanently whitelisted via .env (AGENTOS_UI_ALLOWED_IPS) — "
|
|
253
|
+
"it cannot be disabled through the API; edit .env and restart to change it."
|
|
254
|
+
)
|
|
255
|
+
raw = _load_raw()
|
|
256
|
+
entries = [x for x in _parse_ip_list(raw.get("allowed_ips", "")) if x != ip]
|
|
257
|
+
raw["allowed_ips"] = ", ".join(entries)
|
|
258
|
+
_save_raw(raw)
|
|
259
|
+
return get_allowed_ips()
|
|
260
|
+
|
|
261
|
+
|
|
193
262
|
def get_branch_name_template() -> str | None:
|
|
194
263
|
"""Org-wide branch-naming convention, if configured in config.toml.
|
|
195
264
|
|
|
@@ -59,6 +59,67 @@ async def status() -> dict:
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
# ── IP allowlist management ─────────────────────────────────────────────────
|
|
63
|
+
# Whitelist a client IP for THIS SDK. A whitelisted IP passes the IP gate for
|
|
64
|
+
# EVERY path (all /api/ui/* endpoints + the UI + /docs); data endpoints still
|
|
65
|
+
# additionally require the co_ token. Persisted to config.toml `allowed_ips`; the
|
|
66
|
+
# middleware re-reads it per request so a new IP works immediately (no restart).
|
|
67
|
+
# The backend proxies here (POST /api/v1/sdk/whitelist-ip) to open access for an IP.
|
|
68
|
+
|
|
69
|
+
def _valid_ip_or_cidr(value: str) -> bool:
|
|
70
|
+
import ipaddress
|
|
71
|
+
try:
|
|
72
|
+
ipaddress.ip_network(value, strict=False)
|
|
73
|
+
return True
|
|
74
|
+
except ValueError:
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@router.get("/allowlist", dependencies=[_auth])
|
|
79
|
+
async def get_allowlist() -> dict:
|
|
80
|
+
"""This SDK's allowlist, split into permanent vs runtime.
|
|
81
|
+
|
|
82
|
+
- ``permanent`` — from ``.env`` (AGENTOS_UI_ALLOWED_IPS); always allowed, not
|
|
83
|
+
disableable via the API.
|
|
84
|
+
- ``runtime`` — whitelisted on request; stay until disabled.
|
|
85
|
+
(loopback + this host's own IPs are always allowed, implicitly.)
|
|
86
|
+
"""
|
|
87
|
+
from sdk.config import get_allowed_ips, permanent_allowed_ips, runtime_allowed_ips
|
|
88
|
+
return {
|
|
89
|
+
"allowed_ips": get_allowed_ips(),
|
|
90
|
+
"permanent": permanent_allowed_ips(),
|
|
91
|
+
"runtime": runtime_allowed_ips(),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@router.post("/allowlist", dependencies=[_auth])
|
|
96
|
+
async def add_to_allowlist(body: dict) -> dict:
|
|
97
|
+
"""Whitelist an IP/CIDR for this SDK at runtime. Body: ``{ip}``. Idempotent.
|
|
98
|
+
|
|
99
|
+
Opens the IP gate for that client across every path (all endpoints + UI + docs)
|
|
100
|
+
until it's disabled. Effective immediately (the middleware re-reads per request).
|
|
101
|
+
"""
|
|
102
|
+
from sdk.config import add_allowed_ip
|
|
103
|
+
|
|
104
|
+
ip = str(body.get("ip", "")).strip()
|
|
105
|
+
if not ip:
|
|
106
|
+
raise HTTPException(status_code=400, detail="ip is required.")
|
|
107
|
+
if not _valid_ip_or_cidr(ip):
|
|
108
|
+
raise HTTPException(status_code=400, detail=f"invalid IP or CIDR: {ip!r}")
|
|
109
|
+
return {"added": ip, "allowed_ips": add_allowed_ip(ip)}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@router.delete("/allowlist", dependencies=[_auth])
|
|
113
|
+
async def remove_from_allowlist(ip: str = Query(...)) -> dict:
|
|
114
|
+
"""Disable a runtime-whitelisted IP/CIDR. Permanent (``.env``) entries → 400."""
|
|
115
|
+
from sdk.config import remove_allowed_ip, PermanentAllowlistEntry
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
return {"removed": ip, "allowed_ips": remove_allowed_ip(ip)}
|
|
119
|
+
except PermanentAllowlistEntry as exc:
|
|
120
|
+
raise HTTPException(status_code=400, detail=str(exc))
|
|
121
|
+
|
|
122
|
+
|
|
62
123
|
@router.post("/configure", dependencies=[_auth])
|
|
63
124
|
async def configure(body: dict) -> dict:
|
|
64
125
|
from sdk import config as sdk_config
|
|
@@ -204,75 +265,95 @@ async def connectors() -> dict:
|
|
|
204
265
|
|
|
205
266
|
@router.get("/agents", dependencies=[_auth])
|
|
206
267
|
async def agents() -> dict:
|
|
207
|
-
"""
|
|
208
|
-
from sdk.agents.manifest import load_agents
|
|
209
|
-
from sdk.agents.prompt_vars import extract_variables
|
|
268
|
+
"""The caller's custom agents (``relation_type=person``) from the backend.
|
|
210
269
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
270
|
+
Backend is the source of truth — agents are authored in the web app and read
|
|
271
|
+
here via GET /api/v1/agents (prompt-based shape: prompt = {system_prompt,
|
|
272
|
+
variables:[…]}; skills = [{name,…}]). Best-effort: on backend error the list
|
|
273
|
+
is empty with an ``error`` string the UI can surface.
|
|
274
|
+
"""
|
|
275
|
+
from sdk.client import BackendClient
|
|
276
|
+
from sdk.exceptions import BackendFeatureMissing
|
|
277
|
+
|
|
278
|
+
try:
|
|
279
|
+
rows = await BackendClient.from_config().list_custom_agents()
|
|
280
|
+
except BackendFeatureMissing:
|
|
281
|
+
return {"agents": [], "error": ("Custom agents aren't available on this "
|
|
282
|
+
"backend — it may be running an older version.")}
|
|
283
|
+
except Exception as exc: # noqa: BLE001 — degrade to empty, surface the reason
|
|
284
|
+
return {"agents": [], "error": str(exc)}
|
|
285
|
+
return {"agents": rows, "error": ""}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _flat_agent(row: dict, source: str, *, chattable: bool, department_id: str = "") -> dict:
|
|
289
|
+
"""Flatten a backend prompt-based ``agents`` row into the picker shape the UI
|
|
290
|
+
expects (``name``/``description``/``variables``/``skills``/``source``)."""
|
|
291
|
+
prompt = row.get("prompt") or {}
|
|
292
|
+
sys_prompt = prompt.get("system_prompt", "") if isinstance(prompt, dict) else ""
|
|
293
|
+
pvars = prompt.get("variables", []) if isinstance(prompt, dict) else []
|
|
294
|
+
entry = {
|
|
295
|
+
"name": row.get("name"),
|
|
296
|
+
"description": sys_prompt[:200],
|
|
297
|
+
"variables": [v.get("name") for v in pvars if isinstance(v, dict) and v.get("name")],
|
|
298
|
+
"skills": row.get("skills", []),
|
|
299
|
+
"status": row.get("status", "active"),
|
|
300
|
+
"source": source,
|
|
301
|
+
"chattable": chattable,
|
|
218
302
|
}
|
|
303
|
+
if department_id:
|
|
304
|
+
entry["department_id"] = department_id
|
|
305
|
+
return entry
|
|
219
306
|
|
|
220
307
|
|
|
221
308
|
@router.get("/agents/all", dependencies=[_auth])
|
|
222
309
|
async def agents_all(department_id: str = Query("")) -> dict:
|
|
223
|
-
"""Every agent a person can pick to chat:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
310
|
+
"""Every agent a person can pick to chat: their custom agents + department
|
|
311
|
+
defaults, both read from the backend (source of truth).
|
|
312
|
+
|
|
313
|
+
Custom agents (``relation_type=person``) come from GET /api/v1/agents; defaults
|
|
314
|
+
(``relation_type=department`` + org-wide common) from GET /api/v1/department-agents.
|
|
315
|
+
Custom wins on a name collision. Each entry carries its detected variables so
|
|
316
|
+
the UI can render input fields. Both are best-effort — an unreachable backend
|
|
317
|
+
yields an empty list plus an error string rather than a hard failure.
|
|
229
318
|
"""
|
|
230
|
-
from sdk.
|
|
231
|
-
from sdk.
|
|
232
|
-
|
|
233
|
-
locals_ = load_agents()
|
|
234
|
-
local_names = {m.name for m in locals_}
|
|
235
|
-
out = [
|
|
236
|
-
{"name": m.name, "engine": m.engine, "description": m.description,
|
|
237
|
-
"variables": extract_variables(m.prompt), "source": "custom",
|
|
238
|
-
"chattable": True}
|
|
239
|
-
for m in locals_
|
|
240
|
-
]
|
|
319
|
+
from sdk.client import BackendClient
|
|
320
|
+
from sdk.exceptions import BackendFeatureMissing
|
|
241
321
|
|
|
322
|
+
out: list[dict] = []
|
|
323
|
+
seen: set[str] = set()
|
|
242
324
|
dept = department_id
|
|
243
325
|
defaults_error = ""
|
|
326
|
+
|
|
244
327
|
try:
|
|
245
|
-
from sdk.client import BackendClient
|
|
246
|
-
from sdk.exceptions import BackendFeatureMissing
|
|
247
328
|
client = BackendClient.from_config()
|
|
248
329
|
if not dept:
|
|
249
330
|
profile = await client.bootstrap()
|
|
250
331
|
dept = getattr(profile, "department_id", None) or ""
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
332
|
+
|
|
333
|
+
# Custom agents first so they win any name collision with a default.
|
|
334
|
+
try:
|
|
335
|
+
for r in await client.list_custom_agents():
|
|
336
|
+
nm = r.get("name")
|
|
337
|
+
if not nm or nm in seen:
|
|
338
|
+
continue
|
|
339
|
+
seen.add(nm)
|
|
340
|
+
out.append(_flat_agent(r, "custom", chattable=True))
|
|
341
|
+
except BackendFeatureMissing:
|
|
342
|
+
defaults_error = ("Custom agents aren't available on this backend — "
|
|
343
|
+
"it may be running an older version.")
|
|
344
|
+
|
|
345
|
+
# Department defaults (a default must be activated/cloned before it runs).
|
|
346
|
+
try:
|
|
347
|
+
for r in await client.list_department_agents():
|
|
262
348
|
nm = r.get("name")
|
|
263
|
-
if not nm or nm in
|
|
349
|
+
if not nm or nm in seen:
|
|
264
350
|
continue
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
"variables": [v.get("name") for v in pvars if isinstance(v, dict) and v.get("name")],
|
|
272
|
-
"skills": r.get("skills", []),
|
|
273
|
-
"source": "default", "department_id": dept, "chattable": False,
|
|
274
|
-
})
|
|
275
|
-
except Exception: # noqa: BLE001 — defaults are best-effort
|
|
351
|
+
seen.add(nm)
|
|
352
|
+
out.append(_flat_agent(r, "default", chattable=False, department_id=dept))
|
|
353
|
+
except BackendFeatureMissing:
|
|
354
|
+
defaults_error = ("Department default agents aren't available on this "
|
|
355
|
+
"backend — it may be running an older version.")
|
|
356
|
+
except Exception: # noqa: BLE001 — best-effort; return whatever we gathered
|
|
276
357
|
pass
|
|
277
358
|
|
|
278
359
|
return {"agents": out, "department_id": dept, "defaults_error": defaults_error}
|
|
@@ -71,18 +71,24 @@ class IPAllowlistMiddleware(BaseHTTPMiddleware):
|
|
|
71
71
|
|
|
72
72
|
def __init__(self, app, allowed: list[str] | None = None) -> None:
|
|
73
73
|
super().__init__(app)
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
|
|
74
|
+
# `allowed` set explicitly (tests) → fixed list. Otherwise the configured
|
|
75
|
+
# allowlist is read FRESH per request (see `_configured_nets`) so an IP
|
|
76
|
+
# whitelisted at runtime via /api/ui/allowlist takes effect immediately —
|
|
77
|
+
# no restart. This host's own IPs are static, so compute them once.
|
|
78
|
+
self._explicit = allowed
|
|
79
|
+
self._local_nets = [n for n in (_parse(a) for a in _local_ips()) if n is not None]
|
|
80
|
+
self._trust_xff = os.environ.get("AGENTOS_UI_TRUST_FORWARDED_FOR", "") in ("1", "true", "TRUE", "yes")
|
|
81
|
+
|
|
82
|
+
def _configured_nets(self) -> list:
|
|
83
|
+
if self._explicit is not None:
|
|
84
|
+
entries = self._explicit
|
|
85
|
+
else:
|
|
77
86
|
try:
|
|
78
87
|
from sdk.config import get_allowed_ips
|
|
79
|
-
|
|
88
|
+
entries = get_allowed_ips()
|
|
80
89
|
except Exception: # noqa: BLE001 — never let config break the server
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
entries = list(allowed) + list(_local_ips())
|
|
84
|
-
self._networks = [n for n in (_parse(a) for a in entries) if n is not None]
|
|
85
|
-
self._trust_xff = os.environ.get("AGENTOS_UI_TRUST_FORWARDED_FOR", "") in ("1", "true", "TRUE", "yes")
|
|
90
|
+
entries = []
|
|
91
|
+
return [n for n in (_parse(a) for a in entries) if n is not None]
|
|
86
92
|
|
|
87
93
|
def _client_ip(self, request: Request) -> str:
|
|
88
94
|
if self._trust_xff:
|
|
@@ -98,7 +104,7 @@ class IPAllowlistMiddleware(BaseHTTPMiddleware):
|
|
|
98
104
|
return False
|
|
99
105
|
if _loopback(ip):
|
|
100
106
|
return True
|
|
101
|
-
return any(ip in net for net in self.
|
|
107
|
+
return any(ip in net for net in (self._local_nets + self._configured_nets()))
|
|
102
108
|
|
|
103
109
|
async def dispatch(self, request: Request, call_next):
|
|
104
110
|
# Guard EVERY path — nothing on this server is public.
|
|
@@ -650,26 +650,40 @@
|
|
|
650
650
|
try {
|
|
651
651
|
const d = await api("/api/ui/agents");
|
|
652
652
|
const rows = d.agents || [];
|
|
653
|
+
if (!rows.length && d.error) {
|
|
654
|
+
$("local-agents").innerHTML = `<div class="empty">${esc(d.error)}</div>`;
|
|
655
|
+
const kaSel0 = $("key-agent");
|
|
656
|
+
if (kaSel0) { kaSel0.innerHTML = `<option value="">(no agents yet)</option>`; onAgentSelect(); }
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
653
659
|
caMeta = {}; rows.forEach(a => { caMeta[a.name] = a; });
|
|
654
660
|
const kaSel = $("key-agent");
|
|
655
661
|
if (kaSel) {
|
|
656
662
|
kaSel.innerHTML = rows.length
|
|
657
|
-
? rows.map(a => `<option value="${esc(a.name)}">${esc(a.name)}
|
|
663
|
+
? rows.map(a => `<option value="${esc(a.name)}">${esc(a.name)}</option>`).join("")
|
|
658
664
|
: `<option value="">(no agents yet)</option>`;
|
|
659
665
|
onAgentSelect();
|
|
660
666
|
}
|
|
661
|
-
$("local-agents").innerHTML = rows.length ? rows.map(a =>
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
667
|
+
$("local-agents").innerHTML = rows.length ? rows.map(a => {
|
|
668
|
+
const prompt = a.prompt || {};
|
|
669
|
+
const summary = (prompt.system_prompt || "").trim().slice(0, 200);
|
|
670
|
+
const nvars = (prompt.variables || []).length;
|
|
671
|
+
const nskills = (a.skills || []).length;
|
|
672
|
+
return `<div class="agent-card">
|
|
673
|
+
<div class="agent-head">
|
|
674
|
+
<span class="agent-name">${esc(a.name)}</span>
|
|
675
|
+
<span class="pill ${a.status === "disabled" ? "off" : "on"}">${esc(a.status || "active")}</span>
|
|
676
|
+
</div>
|
|
677
|
+
<div class="agent-meta">${nvars} variable(s) · ${nskills} skill(s)</div>
|
|
678
|
+
<div class="agent-desc">${esc(summary)}</div>
|
|
666
679
|
<div class="agent-actions">
|
|
667
680
|
<a class="btn primary" href="/agents.html?agent=${encodeURIComponent(a.name)}">Run</a>
|
|
668
681
|
<button class="btn" data-enable="${esc(a.name)}">Enable</button>
|
|
669
682
|
<button class="btn" data-disable="${esc(a.name)}">Disable</button>
|
|
670
683
|
<button class="btn danger" data-del="${esc(a.name)}">Delete</button>
|
|
671
684
|
</div>
|
|
672
|
-
</div
|
|
685
|
+
</div>`;
|
|
686
|
+
}).join("")
|
|
673
687
|
: "<div class='empty'>No custom agents assigned to you yet. Agents are created in the web app.</div>";
|
|
674
688
|
|
|
675
689
|
$("local-agents").querySelectorAll("[data-enable]").forEach(b => b.onclick = () => setStatus(b.dataset.enable, "active"));
|
|
File without changes
|
|
File without changes
|
{devops_bot_sdk-1.4.119 → devops_bot_sdk-1.4.124}/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
|