devops-bot-sdk 1.6.4__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.4 → devops_bot_sdk-1.6.6}/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agentd.py +8 -6
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/chat.py +56 -10
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/manifest.py +59 -2
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/secrets.py +9 -1
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/ticket_run.py +15 -14
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/cli.py +14 -22
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_agent_run_ticket.py +56 -14
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_agents_connectors_builder.py +16 -2
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/README.md +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/SOURCES.txt +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/pyproject.toml +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/activity.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/client.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/config.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/github_ingest.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/test.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/api.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/ip_allowlist.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/models.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/server.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/session.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/static/login.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/sdk/vps_health.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/setup.cfg +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_agent_activity.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_agent_listing.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_github_ingest.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_otp_login.py +0 -0
- {devops_bot_sdk-1.6.4 → devops_bot_sdk-1.6.6}/tests/test_updater_extras.py +0 -0
- {devops_bot_sdk-1.6.4 → 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)
|
|
@@ -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(
|
|
@@ -333,35 +333,27 @@ def _agent_run_ticket(name: str, flags: list[str]) -> None:
|
|
|
333
333
|
|
|
334
334
|
|
|
335
335
|
def _agent_run(name: str) -> None:
|
|
336
|
-
|
|
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
|
|
337
346
|
from sdk.client import BackendClient
|
|
338
347
|
from sdk import agentd
|
|
339
348
|
|
|
340
349
|
client = BackendClient.from_config()
|
|
341
|
-
|
|
342
|
-
matched = next((m for m in load_agents() if m.name == name), None)
|
|
343
|
-
if matched is not None:
|
|
344
|
-
asyncio.run(agentd._dispatch_agent_run(matched, client))
|
|
345
|
-
return
|
|
346
|
-
|
|
347
|
-
# Not a local personal agent -- check this user's department defaults
|
|
348
|
-
# (read-only, pulled from the backend; see AgentDaemon._load_department_agents).
|
|
349
350
|
try:
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
)
|
|
354
|
-
except Exception as exc: # noqa: BLE001
|
|
355
|
-
print(f"error: could not fetch department agents ({exc})")
|
|
356
|
-
sys.exit(1)
|
|
357
|
-
|
|
358
|
-
if dept_row is None:
|
|
359
|
-
print(f"error: no agent named '{name}' (checked ~/.agentos/agents/ and "
|
|
360
|
-
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}")
|
|
361
354
|
sys.exit(1)
|
|
362
355
|
|
|
363
|
-
|
|
364
|
-
asyncio.run(agentd._dispatch_agent_run(dept_manifest, client))
|
|
356
|
+
asyncio.run(agentd._dispatch_agent_run(manifest, client))
|
|
365
357
|
|
|
366
358
|
|
|
367
359
|
def _agent_logs(name: str) -> None:
|
|
@@ -110,42 +110,84 @@ async def test_policy_always_without_auto_approve_waits_for_human():
|
|
|
110
110
|
# ── ticket_run helper ───────────────────────────────────────────────────────
|
|
111
111
|
|
|
112
112
|
class _ResolveClient:
|
|
113
|
-
def __init__(self, dept_rows=None, tasks=None):
|
|
113
|
+
def __init__(self, dept_rows=None, tasks=None, custom_rows=None):
|
|
114
114
|
self._dept = dept_rows or []
|
|
115
|
+
self._custom = custom_rows or []
|
|
115
116
|
self._tasks = tasks or []
|
|
116
117
|
|
|
117
118
|
async def list_department_agents(self):
|
|
118
119
|
return self._dept
|
|
119
120
|
|
|
121
|
+
async def list_custom_agents(self):
|
|
122
|
+
return self._custom
|
|
123
|
+
|
|
120
124
|
async def list_tasks_by_keys(self, keys):
|
|
121
125
|
return self._tasks
|
|
122
126
|
|
|
123
127
|
|
|
128
|
+
def _row(name, prompt="Extract the latest AI news and summarize it.", status="active",
|
|
129
|
+
skills=()):
|
|
130
|
+
"""A backend prompt-based ``agents`` row (docs/DATABASE_SCHEMA.md §4).
|
|
131
|
+
|
|
132
|
+
There is no ``manifest`` column — it was dropped in the prompt-based
|
|
133
|
+
migration, which is why reading one made every backend agent unrunnable.
|
|
134
|
+
"""
|
|
135
|
+
return {"name": name, "status": status,
|
|
136
|
+
"prompt": {"system_prompt": prompt, "variables": []},
|
|
137
|
+
"skills": [{"name": n, "description": d} for n, d in skills]}
|
|
138
|
+
|
|
139
|
+
|
|
124
140
|
@pytest.mark.asyncio
|
|
125
|
-
async def
|
|
141
|
+
async def test_resolve_manifest_custom_prefers_local_then_backend():
|
|
126
142
|
m = _stub_manifest()
|
|
127
|
-
|
|
143
|
+
# A local manifest wins. chat._find_agent binds load_agents at import, so
|
|
144
|
+
# patch the name the resolver actually calls.
|
|
145
|
+
with patch("sdk.agents.chat.load_agents", return_value=[m]):
|
|
128
146
|
got = await ticket_run.resolve_manifest(_ResolveClient(), "Tix", scope="custom")
|
|
129
147
|
assert got is m
|
|
130
|
-
|
|
148
|
+
|
|
149
|
+
# No local manifest → the caller's own backend agents are used.
|
|
150
|
+
with patch("sdk.agents.chat.load_agents", return_value=[]):
|
|
151
|
+
client = _ResolveClient(custom_rows=[_row("AI Research Agent")])
|
|
152
|
+
got = await ticket_run.resolve_manifest(client, "AI Research Agent", scope="custom")
|
|
153
|
+
assert got is not None and got.name == "AI Research Agent"
|
|
154
|
+
assert got.engine == "claude_code" and got.trigger.type == "manual"
|
|
155
|
+
|
|
131
156
|
assert await ticket_run.resolve_manifest(_ResolveClient(), "Nope") is None
|
|
132
157
|
|
|
133
158
|
|
|
134
159
|
@pytest.mark.asyncio
|
|
135
160
|
async def test_resolve_manifest_department_active_only():
|
|
136
|
-
rows = [
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
"manifest": {"name": "DeptA", "prompt": "p", "engine": "claude_code"}},
|
|
140
|
-
]
|
|
141
|
-
client = _ResolveClient(dept_rows=rows)
|
|
142
|
-
# resolve_manifest does `from sdk.agents.manifest import AgentManifest` at call
|
|
143
|
-
# time, so patch it on that module; build a stand-in from the manifest dict.
|
|
144
|
-
with patch("sdk.agents.manifest.AgentManifest", side_effect=lambda **kw: SimpleNamespace(**kw)):
|
|
145
|
-
got = await ticket_run.resolve_manifest(client, "DeptA", scope="department")
|
|
161
|
+
rows = [_row("DeptA", status="inactive"), _row("DeptA", status="active")]
|
|
162
|
+
got = await ticket_run.resolve_manifest(
|
|
163
|
+
_ResolveClient(dept_rows=rows), "DeptA", scope="department")
|
|
146
164
|
assert got is not None and got.name == "DeptA"
|
|
147
165
|
|
|
148
166
|
|
|
167
|
+
@pytest.mark.asyncio
|
|
168
|
+
async def test_resolve_manifest_department_with_empty_prompt_is_not_runnable():
|
|
169
|
+
"""Every department row on the live DB has an empty system_prompt — there is
|
|
170
|
+
no instruction to execute, so it must not resolve to a bogus manifest."""
|
|
171
|
+
rows = [_row("Model QA Specialist", prompt="")]
|
|
172
|
+
got = await ticket_run.resolve_manifest(
|
|
173
|
+
_ResolveClient(dept_rows=rows), "Model QA Specialist", scope="department")
|
|
174
|
+
assert got is None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@pytest.mark.asyncio
|
|
178
|
+
async def test_backend_skills_are_folded_into_the_prompt():
|
|
179
|
+
"""Skill names on a row point at BACKEND skills rows; left in manifest.skills
|
|
180
|
+
they would be dropped as dangling local references, losing the text."""
|
|
181
|
+
client = _ResolveClient(custom_rows=[
|
|
182
|
+
_row("Researcher", skills=(("web-search", "Search the web for sources."),))])
|
|
183
|
+
with patch("sdk.agents.chat.load_agents", return_value=[]):
|
|
184
|
+
got = await ticket_run.resolve_manifest(client, "Researcher", scope="custom")
|
|
185
|
+
assert got is not None
|
|
186
|
+
assert "## Skill: web-search" in got.prompt
|
|
187
|
+
assert "Search the web for sources." in got.prompt
|
|
188
|
+
assert got.skills == []
|
|
189
|
+
|
|
190
|
+
|
|
149
191
|
@pytest.mark.asyncio
|
|
150
192
|
async def test_run_on_ticket_missing_ticket_raises():
|
|
151
193
|
client = _ResolveClient(tasks=[])
|
|
@@ -257,9 +257,23 @@ def test_build_agent_enforce_template_flag():
|
|
|
257
257
|
|
|
258
258
|
|
|
259
259
|
def test_chat_module_finds_missing_agent():
|
|
260
|
-
|
|
260
|
+
"""_find_agent is LOCAL-only and returns None; resolve_agent raises after it
|
|
261
|
+
has also checked the backend (that is where agents live now)."""
|
|
262
|
+
import asyncio
|
|
263
|
+
|
|
264
|
+
from sdk.agents.chat import _find_agent, resolve_agent
|
|
265
|
+
|
|
266
|
+
assert _find_agent("no-such-agent-xyz") is None
|
|
267
|
+
|
|
268
|
+
class _NoAgents:
|
|
269
|
+
async def list_custom_agents(self):
|
|
270
|
+
return []
|
|
271
|
+
|
|
272
|
+
async def list_department_agents(self):
|
|
273
|
+
return []
|
|
274
|
+
|
|
261
275
|
with pytest.raises(LookupError):
|
|
262
|
-
|
|
276
|
+
asyncio.run(resolve_agent("no-such-agent-xyz", _NoAgents()))
|
|
263
277
|
|
|
264
278
|
|
|
265
279
|
def test_save_agent_writes_manifest(tmp_path, monkeypatch):
|
|
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
|
|
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
|