devops-bot-sdk 1.6.2__tar.gz → 1.6.6__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.6.2 → devops_bot_sdk-1.6.6}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agentd.py +8 -6
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/chat.py +56 -10
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/inventory.py +6 -4
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/manifest.py +59 -2
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/secrets.py +9 -1
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/ticket_run.py +15 -14
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/cli.py +80 -51
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/client.py +18 -5
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/api.py +56 -38
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/static/app.js +8 -1
- devops_bot_sdk-1.6.6/tests/test_agent_listing.py +170 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_agent_run_ticket.py +56 -14
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_agents_connectors_builder.py +16 -2
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/README.md +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/static/login.html +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_otp_login.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.2 → devops_bot_sdk-1.6.6}/tests/test_vps_health.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.6.
|
|
3
|
+
Version: 1.6.6
|
|
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.6.
|
|
33
|
+
__version__ = "1.6.6" # 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
|
|
@@ -543,7 +543,7 @@ class AgentDaemon:
|
|
|
543
543
|
validate) is logged and skipped — it must never prevent PERSONAL
|
|
544
544
|
agents from loading.
|
|
545
545
|
"""
|
|
546
|
-
from sdk.agents.manifest import
|
|
546
|
+
from sdk.agents.manifest import manifest_from_backend_row
|
|
547
547
|
|
|
548
548
|
try:
|
|
549
549
|
rows = await self.client.list_department_agents()
|
|
@@ -555,13 +555,15 @@ class AgentDaemon:
|
|
|
555
555
|
for row in rows:
|
|
556
556
|
if row.get("status") != "active":
|
|
557
557
|
continue
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
558
|
+
# Prompt-based rows: the `manifest` column this used to read was
|
|
559
|
+
# dropped from the table, so every row raised KeyError here.
|
|
560
|
+
manifest = manifest_from_backend_row(row)
|
|
561
|
+
if manifest is None:
|
|
561
562
|
logger.warning(
|
|
562
|
-
"agentd.
|
|
563
|
-
row.get("name"), exc,
|
|
563
|
+
"agentd.department_agent_not_runnable name=%s", row.get("name"),
|
|
564
564
|
)
|
|
565
|
+
continue
|
|
566
|
+
manifests.append(manifest)
|
|
565
567
|
return manifests
|
|
566
568
|
|
|
567
569
|
async def watch_tickets(self) -> None:
|
|
@@ -41,14 +41,60 @@ def _tee_events(log_path, on_event: "EventHandler | None") -> "EventHandler":
|
|
|
41
41
|
EventHandler = Callable[[dict], Awaitable[None]]
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
def _find_agent(name: str) -> AgentManifest:
|
|
45
|
-
|
|
46
|
-
if
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
44
|
+
def _find_agent(name: str) -> AgentManifest | None:
|
|
45
|
+
"""A LOCAL manifest by name (``~/.agentos/agents``), or None."""
|
|
46
|
+
return next((m for m in load_agents() if m.name == name), None)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def resolve_agent(name: str, client=None) -> AgentManifest:
|
|
50
|
+
"""Find the agent to run: local manifest → your backend agents → department.
|
|
51
|
+
|
|
52
|
+
The BACKEND is where agents live now — they are authored in the web app, and
|
|
53
|
+
nothing writes ``~/.agentos/agents`` anymore. Resolving only locally meant
|
|
54
|
+
every agent the UI could list was unrunnable ("no local agent named …"), for
|
|
55
|
+
department defaults as much as personal ones. A local manifest still wins when
|
|
56
|
+
present, so a hand-written one keeps overriding its backend namesake.
|
|
57
|
+
|
|
58
|
+
``client`` lets a caller that already has one pass it in rather than having a
|
|
59
|
+
second one built here behind its back.
|
|
60
|
+
|
|
61
|
+
Raises ``LookupError`` naming every place that was checked and why.
|
|
62
|
+
"""
|
|
63
|
+
local = _find_agent(name)
|
|
64
|
+
if local is not None:
|
|
65
|
+
return local
|
|
66
|
+
|
|
67
|
+
from sdk.agents.manifest import manifest_from_backend_row
|
|
68
|
+
|
|
69
|
+
if client is None:
|
|
70
|
+
from sdk.client import BackendClient
|
|
71
|
+
client = BackendClient.from_config()
|
|
72
|
+
|
|
73
|
+
reasons: list[str] = []
|
|
74
|
+
for label, attr in (("your agents", "list_custom_agents"),
|
|
75
|
+
("department defaults", "list_department_agents")):
|
|
76
|
+
fetch = getattr(client, attr, None)
|
|
77
|
+
if fetch is None:
|
|
78
|
+
continue
|
|
79
|
+
try:
|
|
80
|
+
rows = await fetch() or []
|
|
81
|
+
except Exception as exc: # noqa: BLE001 — a fetch failure is not "not found"
|
|
82
|
+
reasons.append(f"{label} could not be fetched ({str(exc) or type(exc).__name__})")
|
|
83
|
+
continue
|
|
84
|
+
for row in rows:
|
|
85
|
+
if row.get("name") == name:
|
|
86
|
+
manifest = manifest_from_backend_row(row)
|
|
87
|
+
if manifest is not None:
|
|
88
|
+
return manifest
|
|
89
|
+
reasons.append(
|
|
90
|
+
f"'{name}' is in your {label} but its prompt is empty, so there "
|
|
91
|
+
f"is no instruction to run — add one in the web app"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
detail = "; ".join(reasons) if reasons else (
|
|
95
|
+
"not in ~/.agentos/agents/, your backend agents, or your department defaults"
|
|
96
|
+
)
|
|
97
|
+
raise LookupError(f"no agent named '{name}' — {detail}")
|
|
52
98
|
|
|
53
99
|
|
|
54
100
|
def _render_conversation(history: list[dict] | None, message: str) -> str:
|
|
@@ -87,7 +133,7 @@ async def chat_with_agent(
|
|
|
87
133
|
``files_changed``, ``error``). ``claude_code``/``model`` engines run through
|
|
88
134
|
the local Claude executor; ``shell`` runs the composed prompt as a command.
|
|
89
135
|
"""
|
|
90
|
-
manifest =
|
|
136
|
+
manifest = await resolve_agent(name)
|
|
91
137
|
|
|
92
138
|
skills = load_skills()
|
|
93
139
|
skill_text = resolve_skill_text(manifest, skills)
|
|
@@ -164,7 +210,7 @@ async def run_agent(
|
|
|
164
210
|
``{{variable}}`` is left unfilled — the UI gates the Run button on this, and
|
|
165
211
|
this is the server-side backstop.
|
|
166
212
|
"""
|
|
167
|
-
manifest =
|
|
213
|
+
manifest = await resolve_agent(name)
|
|
168
214
|
|
|
169
215
|
skills = load_skills()
|
|
170
216
|
skill_text = resolve_skill_text(manifest, skills)
|
|
@@ -37,17 +37,19 @@ def _map_backend_agent(row: dict, source: str) -> dict:
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
async def _custom_agents() -> list[dict]:
|
|
40
|
-
"""The caller's custom agents (``relation_type=
|
|
40
|
+
"""The caller's own custom agents (``relation_type='User'``) from the backend.
|
|
41
41
|
|
|
42
|
-
Backend is the source of truth — agents are authored in the web app
|
|
43
|
-
via GET /api/v1/agents. Best-effort:
|
|
42
|
+
Backend is the source of truth — agents are authored in the web app, owned by
|
|
43
|
+
the caller's ``users.id``, and read via GET /api/v1/agents. Best-effort:
|
|
44
|
+
degrades to empty on any error, but logs at WARNING because "this user owns no
|
|
45
|
+
agents" and "the fetch failed" look identical downstream once both are ``[]``.
|
|
44
46
|
"""
|
|
45
47
|
try:
|
|
46
48
|
from sdk.client import BackendClient
|
|
47
49
|
rows = await BackendClient.from_config().list_custom_agents()
|
|
48
50
|
return [_map_backend_agent(r, "custom") for r in rows]
|
|
49
51
|
except Exception as exc: # noqa: BLE001
|
|
50
|
-
logger.
|
|
52
|
+
logger.warning("custom-agent inventory failed (listing as empty): %s", exc)
|
|
51
53
|
return []
|
|
52
54
|
|
|
53
55
|
|
|
@@ -23,8 +23,14 @@ INTEGRATIONS_DIR = Path.home() / ".agentos" / "integrations"
|
|
|
23
23
|
# Full-string match: ${ENV_VAR_NAME} — no inline secrets allowed in integration env.
|
|
24
24
|
_ENV_PLACEHOLDER_RE = re.compile(r"^\$\{[A-Za-z_][A-Za-z0-9_]*\}$")
|
|
25
25
|
|
|
26
|
-
# Agent names
|
|
27
|
-
|
|
26
|
+
# Agent names. Agents are authored in the WEB APP now, where names are free text
|
|
27
|
+
# — "AI Research Agent", "Jira & Confluence Project Management Agent". A rule of
|
|
28
|
+
# `[A-Za-z0-9_-]+` rejected every one of them, so a backend agent could not be
|
|
29
|
+
# materialized into a manifest and therefore could not be run at all. Spaces and
|
|
30
|
+
# ordinary punctuation are allowed; what stays banned is anything that could
|
|
31
|
+
# escape a path or break a log line, since the name is used to derive filenames
|
|
32
|
+
# (see agents/secrets.py, varstore.py, run_log.py — all of which sanitize too).
|
|
33
|
+
_AGENT_NAME_RE = re.compile(r"^[^\x00-\x1f/\\<>:\"|?*]+$")
|
|
28
34
|
|
|
29
35
|
# Flags an inline secret like `token: abc123...` in free-text fields while
|
|
30
36
|
# allowing the ${ENV_VAR} placeholder form. Mirrors the backend
|
|
@@ -210,6 +216,57 @@ def assert_secret_free(obj: object, _where: str = "payload") -> None:
|
|
|
210
216
|
)
|
|
211
217
|
|
|
212
218
|
|
|
219
|
+
def manifest_from_backend_row(row: dict) -> AgentManifest | None:
|
|
220
|
+
"""Materialize a runnable manifest from a backend prompt-based ``agents`` row.
|
|
221
|
+
|
|
222
|
+
Agents are authored in the web app, so the row — not a local YAML file — is
|
|
223
|
+
what the engine has to run. The row shape (docs/DATABASE_SCHEMA.md §4) is
|
|
224
|
+
``{name, status, prompt: {system_prompt, variables:[…]}, skills:[{name,
|
|
225
|
+
description, …}]}``; there is no ``engine``/``trigger``/``manifest`` column
|
|
226
|
+
anymore, so those are supplied here:
|
|
227
|
+
|
|
228
|
+
* ``engine="claude_code"`` — the only engine a web-app agent targets.
|
|
229
|
+
* ``trigger=manual`` — the row carries no trigger; scheduled and
|
|
230
|
+
ticket-label dispatch stay a local-manifest feature.
|
|
231
|
+
|
|
232
|
+
Each attached skill's description is folded into the prompt rather than left
|
|
233
|
+
in ``manifest.skills``: skill names on the row refer to backend ``skills``
|
|
234
|
+
rows, which ``resolve_skill_text`` (local ``~/.agentos/skills``) would drop as
|
|
235
|
+
dangling references, silently losing the instruction text.
|
|
236
|
+
|
|
237
|
+
Returns ``None`` if the row cannot make a valid manifest (no name, or an empty
|
|
238
|
+
/ too-short prompt), so a malformed row is skipped rather than raising.
|
|
239
|
+
"""
|
|
240
|
+
name = str((row or {}).get("name") or "").strip()
|
|
241
|
+
if not name:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
prompt_obj = row.get("prompt") or {}
|
|
245
|
+
if not isinstance(prompt_obj, dict):
|
|
246
|
+
prompt_obj = {}
|
|
247
|
+
prompt = str(prompt_obj.get("system_prompt") or "").strip()
|
|
248
|
+
|
|
249
|
+
for skill in row.get("skills") or []:
|
|
250
|
+
if not isinstance(skill, dict):
|
|
251
|
+
continue
|
|
252
|
+
s_name = str(skill.get("name") or "").strip()
|
|
253
|
+
s_body = str(skill.get("description") or "").strip()
|
|
254
|
+
if s_name and s_body:
|
|
255
|
+
prompt = f"{prompt}\n\n## Skill: {s_name}\n\n{s_body}"
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
return AgentManifest(
|
|
259
|
+
name=name,
|
|
260
|
+
description=prompt[:200],
|
|
261
|
+
engine="claude_code",
|
|
262
|
+
trigger=TriggerSpec(type="manual"),
|
|
263
|
+
prompt=prompt,
|
|
264
|
+
)
|
|
265
|
+
except ValidationError as exc:
|
|
266
|
+
logger.warning("backend agent '%s' is not runnable: %s", name, exc)
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
213
270
|
def load_agents(dir: Path = AGENTS_DIR) -> list[AgentManifest]:
|
|
214
271
|
"""Load every agent manifest under `dir`.
|
|
215
272
|
|
|
@@ -34,7 +34,15 @@ SECRETS_DIR = Path.home() / ".agentos" / "agent_secrets"
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
def _path(agent_name: str) -> Path:
|
|
37
|
-
|
|
37
|
+
"""Store file for an agent's keys.
|
|
38
|
+
|
|
39
|
+
The name is sanitized because agent names are free text from the web app now
|
|
40
|
+
("Jira & Confluence Project Management Agent") — an unsanitized name
|
|
41
|
+
containing a path separator would resolve outside SECRETS_DIR. Mirrors
|
|
42
|
+
``varstore._path`` so the two stores key the same way.
|
|
43
|
+
"""
|
|
44
|
+
safe = "".join(c for c in (agent_name or "") if c.isalnum() or c in "-_") or "agent"
|
|
45
|
+
return SECRETS_DIR / f"{safe}.json.enc"
|
|
38
46
|
|
|
39
47
|
|
|
40
48
|
def load_secrets(agent_name: str) -> dict[str, str]:
|
|
@@ -25,15 +25,19 @@ async def resolve_manifest(
|
|
|
25
25
|
) -> "AgentManifest | None":
|
|
26
26
|
"""Find the agent manifest to run, by ``scope``.
|
|
27
27
|
|
|
28
|
-
* ``custom`` (default) — a personal agent from the local manifests
|
|
29
|
-
(``~/.agentos/agents``).
|
|
30
28
|
* ``department`` — an ACTIVE department default pulled from the backend
|
|
31
|
-
(``list_department_agents``)
|
|
32
|
-
|
|
29
|
+
(``list_department_agents``) and materialized read-only (never written back).
|
|
30
|
+
* ``custom`` (default) — a local manifest, then the caller's own backend
|
|
31
|
+
agents, then their department defaults, via ``chat.resolve_agent``.
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
Backend rows are prompt-based (``prompt.system_prompt`` + attached skills);
|
|
34
|
+
the ``manifest`` column this used to read no longer exists on the table, so
|
|
35
|
+
reading it raised ``KeyError`` and made every department agent unrunnable.
|
|
36
|
+
|
|
37
|
+
Returns None when no matching agent is found, so the caller reports it.
|
|
35
38
|
"""
|
|
36
|
-
from sdk.agents.
|
|
39
|
+
from sdk.agents.chat import resolve_agent
|
|
40
|
+
from sdk.agents.manifest import manifest_from_backend_row
|
|
37
41
|
|
|
38
42
|
name = (name or "").strip()
|
|
39
43
|
if scope == "department":
|
|
@@ -43,16 +47,13 @@ async def resolve_manifest(
|
|
|
43
47
|
return None
|
|
44
48
|
for row in rows:
|
|
45
49
|
if row.get("name") == name and row.get("status") == "active":
|
|
46
|
-
|
|
47
|
-
return AgentManifest(**row["manifest"])
|
|
48
|
-
except Exception: # noqa: BLE001 — invalid manifest → not runnable
|
|
49
|
-
return None
|
|
50
|
+
return manifest_from_backend_row(row)
|
|
50
51
|
return None
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
try:
|
|
54
|
+
return await resolve_agent(name, client)
|
|
55
|
+
except LookupError:
|
|
56
|
+
return None
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
async def run_on_ticket(
|
|
@@ -201,43 +201,80 @@ def _agent_chat(name: str, message: str) -> None:
|
|
|
201
201
|
sys.exit(1)
|
|
202
202
|
|
|
203
203
|
|
|
204
|
+
def _agent_row_summary(row: dict) -> tuple[str, str]:
|
|
205
|
+
"""``(variables, skills)`` display columns for a backend prompt-based row.
|
|
206
|
+
|
|
207
|
+
The live ``agents`` table is prompt-based — ``prompt`` is
|
|
208
|
+
``{system_prompt, variables:[{name,…}]}`` and skills arrive hydrated from the
|
|
209
|
+
``_AgentSkills`` join. The old ``engine``/``trigger_type`` columns this listing
|
|
210
|
+
used to print no longer exist on the row, so they are gone from the table.
|
|
211
|
+
"""
|
|
212
|
+
prompt = row.get("prompt") or {}
|
|
213
|
+
pvars = prompt.get("variables", []) if isinstance(prompt, dict) else []
|
|
214
|
+
names = [v.get("name") for v in pvars if isinstance(v, dict) and v.get("name")]
|
|
215
|
+
skills = [s.get("name") for s in (row.get("skills") or []) if isinstance(s, dict)]
|
|
216
|
+
return ",".join(n for n in names if n), ",".join(s for s in skills if s)
|
|
217
|
+
|
|
218
|
+
|
|
204
219
|
def _agent_list() -> None:
|
|
220
|
+
"""`agentos agent list` — every agent this user can reach.
|
|
221
|
+
|
|
222
|
+
The BACKEND is the source of truth for personal agents: they are authored in the
|
|
223
|
+
web app and stored as ``agents`` rows owned by the caller
|
|
224
|
+
(``relation_type='User'``, ``relation_id=<the caller's users.id>``), read via
|
|
225
|
+
GET /api/v1/agents. Local ``~/.agentos/agents/*.yaml`` manifests are still
|
|
226
|
+
listed — legacy/hand-written ones keep working — but they are no longer the
|
|
227
|
+
only source: since agent creation moved to the web app nothing writes that
|
|
228
|
+
directory, so listing it alone showed every user an empty table even when they
|
|
229
|
+
owned agents server-side.
|
|
230
|
+
"""
|
|
205
231
|
from sdk.agents.manifest import load_agents
|
|
206
232
|
from sdk.client import BackendClient
|
|
207
233
|
|
|
208
234
|
manifests = load_agents()
|
|
209
235
|
|
|
210
|
-
|
|
236
|
+
custom_rows: list[dict] = []
|
|
211
237
|
dept_rows: list[dict] = []
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
for
|
|
238
|
+
errors: list[str] = []
|
|
239
|
+
client = BackendClient.from_config()
|
|
240
|
+
for label, fetch in (("custom", client.list_custom_agents),
|
|
241
|
+
("department", client.list_department_agents)):
|
|
242
|
+
try:
|
|
243
|
+
rows = asyncio.run(fetch()) or []
|
|
244
|
+
except Exception as exc: # noqa: BLE001 — one source failing must not hide the other
|
|
245
|
+
# str() is EMPTY on some httpx errors (ReadTimeout, ConnectTimeout), which
|
|
246
|
+
# would print a warning with no reason at all — name the class then.
|
|
247
|
+
errors.append(f"{label}: {str(exc) or type(exc).__name__}")
|
|
248
|
+
continue
|
|
249
|
+
if label == "custom":
|
|
250
|
+
custom_rows = [r for r in rows if isinstance(r, dict)]
|
|
251
|
+
else:
|
|
252
|
+
dept_rows = [r for r in rows if isinstance(r, dict)]
|
|
253
|
+
|
|
254
|
+
for err in errors:
|
|
255
|
+
print(f"warning: could not fetch {err}", file=sys.stderr)
|
|
256
|
+
|
|
257
|
+
# Local manifests that also exist server-side are the same agent — show once.
|
|
258
|
+
remote_names = {r.get("name") for r in custom_rows}
|
|
259
|
+
local_only = [m for m in manifests if m.name not in remote_names]
|
|
260
|
+
|
|
261
|
+
if not custom_rows and not dept_rows and not local_only:
|
|
262
|
+
print("no agents found: none owned by you on the backend, no department "
|
|
263
|
+
"defaults, and no local manifests in ~/.agentos/agents/")
|
|
264
|
+
if not errors:
|
|
265
|
+
print("(personal agents are created in the web app — they appear here "
|
|
266
|
+
"once saved)")
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
print(f"{'NAME':<32}{'SOURCE':<12}{'STATUS':<10}{'VARIABLES':<24}SKILLS")
|
|
270
|
+
for source, rows in (("personal", custom_rows), ("department", dept_rows)):
|
|
271
|
+
for row in rows:
|
|
272
|
+
variables, skills = _agent_row_summary(row)
|
|
273
|
+
print(f"{str(row.get('name', ''))[:31]:<32}{source:<12}"
|
|
274
|
+
f"{str(row.get('status', ''))[:9]:<10}{variables[:23]:<24}{skills}")
|
|
275
|
+
for m in local_only:
|
|
233
276
|
trigger_type = m.trigger.type if m.trigger else ""
|
|
234
|
-
|
|
235
|
-
print(f"{m.name:<24}{m.engine:<16}{trigger_type:<16}{'personal':<12}{status}")
|
|
236
|
-
for row in dept_rows:
|
|
237
|
-
print(
|
|
238
|
-
f"{row.get('name', ''):<24}{row.get('engine', ''):<16}"
|
|
239
|
-
f"{row.get('trigger_type', ''):<16}{'department':<12}{row.get('status', '')}"
|
|
240
|
-
)
|
|
277
|
+
print(f"{m.name[:31]:<32}{'local':<12}{'-':<10}{trigger_type[:23]:<24}")
|
|
241
278
|
|
|
242
279
|
|
|
243
280
|
def _agent_run_ticket(name: str, flags: list[str]) -> None:
|
|
@@ -296,35 +333,27 @@ def _agent_run_ticket(name: str, flags: list[str]) -> None:
|
|
|
296
333
|
|
|
297
334
|
|
|
298
335
|
def _agent_run(name: str) -> None:
|
|
299
|
-
|
|
336
|
+
"""`agentos agent run NAME` — dispatch one run locally.
|
|
337
|
+
|
|
338
|
+
Resolution order is shared with the UI and `agent chat` (see
|
|
339
|
+
``sdk.agents.chat.resolve_agent``): a local manifest wins, then the caller's
|
|
340
|
+
own backend agents, then their department defaults. It used to check local
|
|
341
|
+
manifests and department rows only — and read a `manifest` column the
|
|
342
|
+
prompt-based `agents` table no longer has — so nothing authored in the web app
|
|
343
|
+
could be run.
|
|
344
|
+
"""
|
|
345
|
+
from sdk.agents.chat import resolve_agent
|
|
300
346
|
from sdk.client import BackendClient
|
|
301
347
|
from sdk import agentd
|
|
302
348
|
|
|
303
349
|
client = BackendClient.from_config()
|
|
304
|
-
|
|
305
|
-
matched = next((m for m in load_agents() if m.name == name), None)
|
|
306
|
-
if matched is not None:
|
|
307
|
-
asyncio.run(agentd._dispatch_agent_run(matched, client))
|
|
308
|
-
return
|
|
309
|
-
|
|
310
|
-
# Not a local personal agent -- check this user's department defaults
|
|
311
|
-
# (read-only, pulled from the backend; see AgentDaemon._load_department_agents).
|
|
312
350
|
try:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
)
|
|
317
|
-
except Exception as exc: # noqa: BLE001
|
|
318
|
-
print(f"error: could not fetch department agents ({exc})")
|
|
319
|
-
sys.exit(1)
|
|
320
|
-
|
|
321
|
-
if dept_row is None:
|
|
322
|
-
print(f"error: no agent named '{name}' (checked ~/.agentos/agents/ and "
|
|
323
|
-
f"this user's department default agents)")
|
|
351
|
+
manifest = asyncio.run(resolve_agent(name, client))
|
|
352
|
+
except LookupError as exc:
|
|
353
|
+
print(f"error: {exc}")
|
|
324
354
|
sys.exit(1)
|
|
325
355
|
|
|
326
|
-
|
|
327
|
-
asyncio.run(agentd._dispatch_agent_run(dept_manifest, client))
|
|
356
|
+
asyncio.run(agentd._dispatch_agent_run(manifest, client))
|
|
328
357
|
|
|
329
358
|
|
|
330
359
|
def _agent_logs(name: str) -> None:
|
|
@@ -1372,12 +1372,25 @@ class BackendClient:
|
|
|
1372
1372
|
# This client only lists/reads them and manages per-agent status/delete.
|
|
1373
1373
|
|
|
1374
1374
|
async def list_custom_agents(self) -> list[dict]:
|
|
1375
|
-
"""GET /api/v1/agents —
|
|
1375
|
+
"""GET /api/v1/agents — the caller's OWN custom agents.
|
|
1376
|
+
|
|
1377
|
+
The backend scopes these to ``agents`` rows whose owner is this caller
|
|
1378
|
+
(``relation_type='User'``, ``relation_id`` = the ``users.id`` it resolves
|
|
1379
|
+
from ``GET /ml-api/users/profile`` for the presented token), so the list is
|
|
1380
|
+
per-user with nothing to pass in. Rows are prompt-based: ``prompt`` =
|
|
1381
|
+
``{system_prompt, variables:[…]}``, plus a hydrated ``skills`` list.
|
|
1382
|
+
|
|
1383
|
+
An empty list means "this user owns no agents" and nothing else — the
|
|
1384
|
+
backend answers 401 when it cannot verify the identity behind the token and
|
|
1385
|
+
404 when the endpoint is absent (``BackendFeatureMissing``), so a silent
|
|
1386
|
+
empty is never a masked failure. Agents are authored in the web app; the SDK
|
|
1387
|
+
is consume-only.
|
|
1388
|
+
"""
|
|
1389
|
+
url = self._url("/api/v1/agents")
|
|
1376
1390
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
1377
|
-
resp = await client.get(
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
)
|
|
1391
|
+
resp = await client.get(url, headers=self._headers)
|
|
1392
|
+
if resp.status_code == 404:
|
|
1393
|
+
raise BackendFeatureMissing("Custom agents", url)
|
|
1381
1394
|
_check_status(resp.status_code, self._base_url)
|
|
1382
1395
|
return resp.json().get("agents", [])
|
|
1383
1396
|
|
|
@@ -499,12 +499,14 @@ async def product_agents(product_id: str = Query(...)) -> dict:
|
|
|
499
499
|
|
|
500
500
|
@router.get("/agents", dependencies=[_auth])
|
|
501
501
|
async def agents() -> dict:
|
|
502
|
-
"""The caller's custom agents (``relation_type=
|
|
503
|
-
|
|
504
|
-
Backend is the source of truth — agents are authored in the web app
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
502
|
+
"""The caller's own custom agents (``relation_type='User'``) from the backend.
|
|
503
|
+
|
|
504
|
+
Backend is the source of truth — agents are authored in the web app, owned by
|
|
505
|
+
the caller's ``users.id`` (which the backend resolves from the profile for the
|
|
506
|
+
presented token), and read here via GET /api/v1/agents (prompt-based shape:
|
|
507
|
+
prompt = {system_prompt, variables:[…]}; skills = [{name,…}]). Best-effort: on
|
|
508
|
+
backend error the list is empty with an ``error`` string the UI can surface, so
|
|
509
|
+
"you own none" stays distinguishable from "the call failed".
|
|
508
510
|
"""
|
|
509
511
|
from sdk.client import BackendClient
|
|
510
512
|
from sdk.exceptions import BackendFeatureMissing
|
|
@@ -544,11 +546,16 @@ async def agents_all(department_id: str = Query("")) -> dict:
|
|
|
544
546
|
"""Every agent a person can pick to chat: their custom agents + department
|
|
545
547
|
defaults, both read from the backend (source of truth).
|
|
546
548
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
549
|
+
The caller's own custom agents (``relation_type='User'``, owned by their
|
|
550
|
+
``users.id``) come from GET /api/v1/agents; defaults (``relation_type='Department'``
|
|
551
|
+
+ org-wide common) from GET /api/v1/department-agents. Custom wins on a name
|
|
552
|
+
collision. Each entry carries its detected variables so the UI can render input
|
|
553
|
+
fields.
|
|
554
|
+
|
|
555
|
+
Both sources are independent and best-effort: one failing never empties the
|
|
556
|
+
other, and each reports its own reason (``custom_error`` / ``defaults_error``)
|
|
557
|
+
so the picker can say WHY it is empty instead of just "(no agents installed)".
|
|
558
|
+
An empty ``agents`` with no error means the user genuinely owns none yet.
|
|
552
559
|
"""
|
|
553
560
|
from sdk.client import BackendClient
|
|
554
561
|
from sdk.exceptions import BackendFeatureMissing
|
|
@@ -556,41 +563,52 @@ async def agents_all(department_id: str = Query("")) -> dict:
|
|
|
556
563
|
out: list[dict] = []
|
|
557
564
|
seen: set[str] = set()
|
|
558
565
|
dept = department_id
|
|
566
|
+
custom_error = ""
|
|
559
567
|
defaults_error = ""
|
|
560
568
|
|
|
561
569
|
try:
|
|
562
570
|
client = BackendClient.from_config()
|
|
563
|
-
|
|
571
|
+
except Exception as exc: # noqa: BLE001 — not configured / no token
|
|
572
|
+
return {"agents": [], "department_id": dept,
|
|
573
|
+
"custom_error": str(exc), "defaults_error": str(exc)}
|
|
574
|
+
|
|
575
|
+
if not dept:
|
|
576
|
+
try:
|
|
564
577
|
profile = await client.bootstrap()
|
|
565
578
|
dept = getattr(profile, "department_id", None) or ""
|
|
579
|
+
except Exception: # noqa: BLE001 — the department label is cosmetic here
|
|
580
|
+
dept = ""
|
|
566
581
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
582
|
+
# Custom agents first so they win any name collision with a default.
|
|
583
|
+
try:
|
|
584
|
+
for r in await client.list_custom_agents():
|
|
585
|
+
nm = r.get("name")
|
|
586
|
+
if not nm or nm in seen:
|
|
587
|
+
continue
|
|
588
|
+
seen.add(nm)
|
|
589
|
+
out.append(_flat_agent(r, "custom", chattable=True))
|
|
590
|
+
except BackendFeatureMissing:
|
|
591
|
+
custom_error = ("Custom agents aren't available on this backend — "
|
|
592
|
+
"it may be running an older version.")
|
|
593
|
+
except Exception as exc: # noqa: BLE001 — surface it; don't hide it as "none"
|
|
594
|
+
custom_error = f"Couldn't load your agents: {exc}"
|
|
595
|
+
|
|
596
|
+
# Department defaults (a default must be activated/cloned before it runs).
|
|
597
|
+
try:
|
|
598
|
+
for r in await client.list_department_agents():
|
|
599
|
+
nm = r.get("name")
|
|
600
|
+
if not nm or nm in seen:
|
|
601
|
+
continue
|
|
602
|
+
seen.add(nm)
|
|
603
|
+
out.append(_flat_agent(r, "default", chattable=False, department_id=dept))
|
|
604
|
+
except BackendFeatureMissing:
|
|
605
|
+
defaults_error = ("Department default agents aren't available on this "
|
|
606
|
+
"backend — it may be running an older version.")
|
|
607
|
+
except Exception as exc: # noqa: BLE001
|
|
608
|
+
defaults_error = f"Couldn't load department defaults: {exc}"
|
|
592
609
|
|
|
593
|
-
return {"agents": out, "department_id": dept,
|
|
610
|
+
return {"agents": out, "department_id": dept,
|
|
611
|
+
"custom_error": custom_error, "defaults_error": defaults_error}
|
|
594
612
|
|
|
595
613
|
|
|
596
614
|
@router.get("/profile", dependencies=[_auth])
|