devops-bot-sdk 1.4.153__tar.gz → 1.4.159__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.153 → devops_bot_sdk-1.4.159}/PKG-INFO +2 -1
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/PKG-INFO +2 -1
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/SOURCES.txt +10 -1
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/requires.txt +1 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/pyproject.toml +5 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agentd.py +72 -22
- devops_bot_sdk-1.4.159/sdk/agents/activity.py +110 -0
- devops_bot_sdk-1.4.159/sdk/agents/ticket_run.py +97 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/cli.py +74 -4
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/client.py +29 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/config.py +19 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/api.py +148 -15
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/ip_allowlist.py +72 -8
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/models.py +53 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/server.py +40 -1
- devops_bot_sdk-1.4.159/sdk/ui/session.py +76 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/agents.html +16 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/app.js +94 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/custom-agents.html +11 -0
- devops_bot_sdk-1.4.159/sdk/ui/static/login.html +111 -0
- devops_bot_sdk-1.4.159/sdk/vps_health.py +264 -0
- devops_bot_sdk-1.4.159/tests/test_agent_activity.py +93 -0
- devops_bot_sdk-1.4.159/tests/test_agent_run_ticket.py +222 -0
- devops_bot_sdk-1.4.159/tests/test_otp_login.py +178 -0
- devops_bot_sdk-1.4.159/tests/test_vps_health.py +209 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/README.md +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/setup.cfg +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_agent_prompt_async.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_updater_extras.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devops-bot-sdk
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.159
|
|
4
4
|
Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
|
|
5
5
|
Author: noumanaziz2128
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -20,6 +20,7 @@ Requires-Dist: psutil>=5.9; extra == "process"
|
|
|
20
20
|
Provides-Extra: ui
|
|
21
21
|
Requires-Dist: fastapi>=0.110; extra == "ui"
|
|
22
22
|
Requires-Dist: uvicorn>=0.29; extra == "ui"
|
|
23
|
+
Requires-Dist: psutil>=5.9; extra == "ui"
|
|
23
24
|
Provides-Extra: all
|
|
24
25
|
Requires-Dist: Pillow>=10.0; extra == "all"
|
|
25
26
|
Requires-Dist: psutil>=5.9; extra == "all"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devops-bot-sdk
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.159
|
|
4
4
|
Summary: DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app
|
|
5
5
|
Author: noumanaziz2128
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -20,6 +20,7 @@ Requires-Dist: psutil>=5.9; extra == "process"
|
|
|
20
20
|
Provides-Extra: ui
|
|
21
21
|
Requires-Dist: fastapi>=0.110; extra == "ui"
|
|
22
22
|
Requires-Dist: uvicorn>=0.29; extra == "ui"
|
|
23
|
+
Requires-Dist: psutil>=5.9; extra == "ui"
|
|
23
24
|
Provides-Extra: all
|
|
24
25
|
Requires-Dist: Pillow>=10.0; extra == "all"
|
|
25
26
|
Requires-Dist: psutil>=5.9; extra == "all"
|
|
@@ -23,7 +23,9 @@ sdk/sse.py
|
|
|
23
23
|
sdk/test.py
|
|
24
24
|
sdk/test_pipeline.py
|
|
25
25
|
sdk/updater.py
|
|
26
|
+
sdk/vps_health.py
|
|
26
27
|
sdk/agents/__init__.py
|
|
28
|
+
sdk/agents/activity.py
|
|
27
29
|
sdk/agents/builder.py
|
|
28
30
|
sdk/agents/chat.py
|
|
29
31
|
sdk/agents/connectors.py
|
|
@@ -35,6 +37,7 @@ sdk/agents/prompt_template.py
|
|
|
35
37
|
sdk/agents/prompt_vars.py
|
|
36
38
|
sdk/agents/run_log.py
|
|
37
39
|
sdk/agents/secrets.py
|
|
40
|
+
sdk/agents/ticket_run.py
|
|
38
41
|
sdk/agents/usage.py
|
|
39
42
|
sdk/agents/varstore.py
|
|
40
43
|
sdk/agents/engines/__init__.py
|
|
@@ -75,20 +78,26 @@ sdk/ui/runner.py
|
|
|
75
78
|
sdk/ui/security.py
|
|
76
79
|
sdk/ui/server.py
|
|
77
80
|
sdk/ui/service.py
|
|
81
|
+
sdk/ui/session.py
|
|
78
82
|
sdk/ui/setup_manager.py
|
|
79
83
|
sdk/ui/static/agents.html
|
|
80
84
|
sdk/ui/static/app.js
|
|
81
85
|
sdk/ui/static/custom-agents.html
|
|
82
86
|
sdk/ui/static/default-agents.html
|
|
83
87
|
sdk/ui/static/index.html
|
|
88
|
+
sdk/ui/static/login.html
|
|
84
89
|
sdk/ui/static/runs.html
|
|
85
90
|
sdk/ui/static/setup.html
|
|
86
91
|
sdk/ui/static/styles.css
|
|
87
92
|
sdk/ui/static/tickets.html
|
|
93
|
+
tests/test_agent_activity.py
|
|
88
94
|
tests/test_agent_chat_features.py
|
|
89
95
|
tests/test_agent_prompt_async.py
|
|
90
96
|
tests/test_agent_prompt_models.py
|
|
97
|
+
tests/test_agent_run_ticket.py
|
|
91
98
|
tests/test_agents_connectors_builder.py
|
|
92
99
|
tests/test_git_action.py
|
|
93
100
|
tests/test_health.py
|
|
94
|
-
tests/
|
|
101
|
+
tests/test_otp_login.py
|
|
102
|
+
tests/test_updater_extras.py
|
|
103
|
+
tests/test_vps_health.py
|
|
@@ -31,6 +31,11 @@ process = [
|
|
|
31
31
|
ui = [
|
|
32
32
|
"fastapi>=0.110", # sdk/ui — control-panel web app
|
|
33
33
|
"uvicorn>=0.29", # ASGI server for `agentos-ui`
|
|
34
|
+
"psutil>=5.9", # sdk/ui/health.py — CPU/RAM/disk/uptime for GET /api/ui/health
|
|
35
|
+
# AND the 2-hourly allocation PATCH (sdk/vps_health.py). The
|
|
36
|
+
# bootstrap installs `[ui]` only, so without it here every
|
|
37
|
+
# VPS reports "resources.available: False" and the health
|
|
38
|
+
# cron can only send status + IP.
|
|
34
39
|
]
|
|
35
40
|
all = [
|
|
36
41
|
"Pillow>=10.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.154
|
|
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.159" # 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
|
|
@@ -85,6 +85,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
85
85
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
86
86
|
from apscheduler.triggers.cron import CronTrigger
|
|
87
87
|
|
|
88
|
+
from sdk.agents import activity
|
|
88
89
|
from sdk.client import BackendClient
|
|
89
90
|
from sdk.models.requests import OrchestrateRequest
|
|
90
91
|
|
|
@@ -251,7 +252,9 @@ async def _wait_for_human_approval(client: BackendClient, task_id: str) -> str:
|
|
|
251
252
|
|
|
252
253
|
async def _dispatch_agent_run(
|
|
253
254
|
manifest: "AgentManifest", client: BackendClient, task: dict | None = None,
|
|
254
|
-
|
|
255
|
+
*, auto_approve: bool = False,
|
|
256
|
+
extra_variables: dict | None = None, extra_secrets: dict | None = None,
|
|
257
|
+
) -> dict:
|
|
255
258
|
"""Run ONE loaded ``AgentManifest`` end to end, for one trigger firing.
|
|
256
259
|
|
|
257
260
|
1. Resolve skills + (optionally) an MCP config from the manifest.
|
|
@@ -262,7 +265,8 @@ async def _dispatch_agent_run(
|
|
|
262
265
|
run, but pauses at ``awaiting_approval`` instead of executing, exactly
|
|
263
266
|
like the desktop's local-execution hybrid for Jira coding tasks.
|
|
264
267
|
4. Drive the GO-AHEAD / FINALIZE round-trip:
|
|
265
|
-
- ``
|
|
268
|
+
- ``auto_approve`` (a human-initiated on-demand run) OR
|
|
269
|
+
``approval.policy == "never"``: agentd sends the GO-AHEAD itself
|
|
266
270
|
(``approval_reply(..., decision="APPROVED", local_execution_result=None)``)
|
|
267
271
|
the moment the run reaches ``awaiting_approval``.
|
|
268
272
|
- otherwise: wait for a human's WhatsApp reply to land (no early
|
|
@@ -271,7 +275,15 @@ async def _dispatch_agent_run(
|
|
|
271
275
|
``sdk.agents.engines.claude_code.run`` and report the result back via
|
|
272
276
|
a second ``approval_reply(..., local_execution_result={...})`` call,
|
|
273
277
|
which finalizes the run server-side.
|
|
278
|
+
|
|
279
|
+
``auto_approve=True`` is used by the on-demand "run agent on a ticket" path
|
|
280
|
+
(UI / API / CLI), where a human already clicked Run — so the agent's
|
|
281
|
+
``approval.policy`` WhatsApp gate is skipped for this single run.
|
|
282
|
+
|
|
283
|
+
Returns an outcome dict — ``{status, task_id, result, ok, error}`` — for the
|
|
284
|
+
on-demand callers; the daemon's trigger loops ignore the return value.
|
|
274
285
|
"""
|
|
286
|
+
outcome: dict = {"status": "", "task_id": "", "result": "", "ok": False, "error": ""}
|
|
275
287
|
from sdk.agents import integrations as integrations_mod
|
|
276
288
|
from sdk.agents import manifest as manifest_mod
|
|
277
289
|
from sdk.agents import secrets as agent_secrets
|
|
@@ -284,7 +296,15 @@ async def _dispatch_agent_run(
|
|
|
284
296
|
# only when the owner edits the agent. Empty for department agents, whose keys
|
|
285
297
|
# come from the host .env already in os.environ.
|
|
286
298
|
stored_secrets = agent_secrets.load_secrets(manifest.name)
|
|
287
|
-
|
|
299
|
+
# EPHEMERAL secrets: supplied for THIS run only (UI/API/CLI), merged on top of
|
|
300
|
+
# any stored keys and injected as env — NEVER written back to disk, so they
|
|
301
|
+
# vanish when the run ends. Explicit run-time values win over stored ones.
|
|
302
|
+
effective_secrets = {**(stored_secrets or {}), **(extra_secrets or {})}
|
|
303
|
+
if extra_secrets:
|
|
304
|
+
import os as _os
|
|
305
|
+
secret_env = {**_os.environ, **effective_secrets}
|
|
306
|
+
else:
|
|
307
|
+
secret_env = agent_secrets.resolved_env(manifest.name) if stored_secrets else None
|
|
288
308
|
|
|
289
309
|
mcp_config: dict[str, Any] | None = None
|
|
290
310
|
if manifest.integrations:
|
|
@@ -293,21 +313,23 @@ async def _dispatch_agent_run(
|
|
|
293
313
|
manifest.integrations, available, env=secret_env,
|
|
294
314
|
)
|
|
295
315
|
|
|
296
|
-
# EPHEMERAL variables:
|
|
297
|
-
#
|
|
298
|
-
#
|
|
316
|
+
# EPHEMERAL variables: filled FRESH from the ticket (when one is attached) and/or
|
|
317
|
+
# from explicit run-time values — never persisted. Explicit ``extra_variables``
|
|
318
|
+
# win over ticket-derived ones. With neither, unresolved {{vars}} are left for
|
|
319
|
+
# the agent to ask about or default, exactly as before.
|
|
299
320
|
resolved_prompt = manifest.prompt
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
310
|
-
|
|
321
|
+
names = extract_variables(manifest.prompt)
|
|
322
|
+
if names and (task or extra_variables):
|
|
323
|
+
values = values_from_ticket(names, task) if task else {}
|
|
324
|
+
if extra_variables:
|
|
325
|
+
values = {**values, **{k: str(v) for k, v in extra_variables.items() if str(v).strip()}}
|
|
326
|
+
resolved_prompt, missing = substitute_variables(manifest.prompt, values)
|
|
327
|
+
if values:
|
|
328
|
+
logger.info(
|
|
329
|
+
"agentd.vars_filled agent=%s filled=%s missing=%s",
|
|
330
|
+
manifest.name, sorted(values), sorted(missing),
|
|
331
|
+
)
|
|
332
|
+
manifest = manifest.model_copy(update={"prompt": resolved_prompt})
|
|
311
333
|
|
|
312
334
|
if manifest.engine != "claude_code":
|
|
313
335
|
# Only claude_code is wired end-to-end today (sdk/agents/engines/{mcp,
|
|
@@ -317,7 +339,8 @@ async def _dispatch_agent_run(
|
|
|
317
339
|
logger.warning(
|
|
318
340
|
"agentd.engine_not_wired engine=%s agent=%s", manifest.engine, manifest.name,
|
|
319
341
|
)
|
|
320
|
-
|
|
342
|
+
outcome["error"] = f"engine '{manifest.engine}' is not wired (only claude_code)."
|
|
343
|
+
return outcome
|
|
321
344
|
|
|
322
345
|
prompt_text = manifest.prompt
|
|
323
346
|
if skill_text:
|
|
@@ -332,6 +355,9 @@ async def _dispatch_agent_run(
|
|
|
332
355
|
|
|
333
356
|
policy = manifest.approval.policy
|
|
334
357
|
handled: set[str] = set() # session/task ids already round-tripped by this dispatch
|
|
358
|
+
# How this run was triggered — carried into the `agent_activities` line so the
|
|
359
|
+
# feed distinguishes a cron firing from a ticket trigger from a human "Run".
|
|
360
|
+
_activity_kind = "run-ticket" if auto_approve else ("ticket_label" if task else "scheduled")
|
|
335
361
|
|
|
336
362
|
async def _on_status_update(status: str, state: dict) -> None:
|
|
337
363
|
if status != "awaiting_approval":
|
|
@@ -341,8 +367,9 @@ async def _dispatch_agent_run(
|
|
|
341
367
|
return
|
|
342
368
|
handled.add(task_id)
|
|
343
369
|
|
|
344
|
-
if policy == "never":
|
|
345
|
-
# No human in the loop for this
|
|
370
|
+
if auto_approve or policy == "never":
|
|
371
|
+
# No human in the loop for this run — agentd IS the approver
|
|
372
|
+
# (agent policy 'never', or a human already clicked Run on-demand).
|
|
346
373
|
try:
|
|
347
374
|
await client.approval_reply(
|
|
348
375
|
task_id, decision="APPROVED", local_execution_result=None,
|
|
@@ -379,16 +406,36 @@ async def _dispatch_agent_run(
|
|
|
379
406
|
async def _tee(ev: dict) -> None:
|
|
380
407
|
run_log.log_event(_log_path, ev)
|
|
381
408
|
|
|
409
|
+
_started = time.monotonic()
|
|
382
410
|
local_result = await claude_code_engine.run(
|
|
383
|
-
manifest, skill_text, mcp_config, extra_env=
|
|
411
|
+
manifest, skill_text, mcp_config, extra_env=effective_secrets or None,
|
|
384
412
|
on_event=_tee,
|
|
385
413
|
)
|
|
386
414
|
run_log.finish_run(_log_path, local_result)
|
|
415
|
+
# Local ledger + platform `agent_activities` feed. EVERY trigger path
|
|
416
|
+
# reaches this function (cron schedule, ticket label, on-demand
|
|
417
|
+
# run-on-ticket from the UI / CLI / desktop IPC), so a VPS agent run is
|
|
418
|
+
# recorded exactly once regardless of origin — see sdk.agents.activity.
|
|
419
|
+
await activity.record_run(
|
|
420
|
+
manifest.name, _activity_kind, local_result,
|
|
421
|
+
time.monotonic() - _started, client=client,
|
|
422
|
+
detail=f"task {task_id}" if task_id else "",
|
|
423
|
+
)
|
|
424
|
+
outcome.update({
|
|
425
|
+
"task_id": task_id,
|
|
426
|
+
"result": local_result.get("result", ""),
|
|
427
|
+
"ok": bool(local_result.get("ok", True)),
|
|
428
|
+
})
|
|
387
429
|
except Exception as exc: # noqa: BLE001
|
|
388
430
|
logger.warning(
|
|
389
431
|
"agentd.local_run_failed agent=%s task=%s err=%s",
|
|
390
432
|
manifest.name, task_id, exc,
|
|
391
433
|
)
|
|
434
|
+
outcome["error"] = str(exc)
|
|
435
|
+
await activity.record_run(
|
|
436
|
+
manifest.name, _activity_kind, {"ok": False, "error": str(exc)},
|
|
437
|
+
0.0, client=client, detail=f"task {task_id}" if task_id else "",
|
|
438
|
+
)
|
|
392
439
|
return
|
|
393
440
|
|
|
394
441
|
# FINALIZE — same field names sdk.local_exec.run_claude_local's result
|
|
@@ -417,11 +464,14 @@ async def _dispatch_agent_run(
|
|
|
417
464
|
result = await client.orchestrate(req, on_status_update=_on_status_update)
|
|
418
465
|
except Exception as exc: # noqa: BLE001
|
|
419
466
|
logger.warning("agentd.orchestrate_failed agent=%s err=%s", manifest.name, exc)
|
|
420
|
-
|
|
467
|
+
outcome["error"] = str(exc)
|
|
468
|
+
return outcome
|
|
421
469
|
|
|
422
470
|
logger.info(
|
|
423
471
|
"agentd.run_complete agent=%s status=%s", manifest.name, getattr(result, "status", ""),
|
|
424
472
|
)
|
|
473
|
+
outcome["status"] = getattr(result, "status", "") or ""
|
|
474
|
+
return outcome
|
|
425
475
|
|
|
426
476
|
|
|
427
477
|
# ── The daemon ───────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Agent-activity reporting — the ONE place a custom / department agent run is
|
|
2
|
+
recorded, for both sinks:
|
|
3
|
+
|
|
4
|
+
* the LOCAL usage ledger (``sdk.agents.usage`` → ``~/.agentos/usage.jsonl``),
|
|
5
|
+
which backs ``GET /api/ui/agents/usage`` and, through it, the AI backend
|
|
6
|
+
report's "VPS Agent Logs" section, and
|
|
7
|
+
* the PLATFORM activity feed — ``POST {ML_GATEWAY}/ml-api/agent-activity``
|
|
8
|
+
with ``{activity, agentId}``, which the ML backend writes into the Postgres
|
|
9
|
+
``agent_activities`` table (docs/DATABASE_SCHEMA.md #3: ``user_id``,
|
|
10
|
+
``activity``, ``agent_id``) and the AI backend reads back RBAC-scoped for
|
|
11
|
+
reporting + chat.
|
|
12
|
+
|
|
13
|
+
Every VPS-side agent run goes through here no matter how it was triggered — the
|
|
14
|
+
control panel (chat / run / run-on-ticket), an ``agentd`` cron schedule, an
|
|
15
|
+
``agentd`` ticket-label trigger, the CLI, or the desktop IPC bridge. Before this
|
|
16
|
+
module only the control-panel paths emitted, so scheduled and ticket-triggered
|
|
17
|
+
runs never reached ``agent_activities``.
|
|
18
|
+
|
|
19
|
+
``agent_activities.agent_id`` is an FK onto ``agents.id``, so only agents that
|
|
20
|
+
exist in the BACKEND emit — ``BackendClient.resolve_agent_id`` returns None for
|
|
21
|
+
a local-only manifest and the emit is skipped (the local ledger still records
|
|
22
|
+
it). Both custom and department-default agents resolve, so both are tracked.
|
|
23
|
+
|
|
24
|
+
Best-effort throughout: nothing in here raises into a run.
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import logging
|
|
29
|
+
from typing import TYPE_CHECKING, Any
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING: # pragma: no cover — typing only
|
|
32
|
+
from sdk.client import BackendClient
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger("sdk.agents.activity")
|
|
35
|
+
|
|
36
|
+
# How a run reached us → the verb that opens the activity line. Keep these short:
|
|
37
|
+
# the feed is rendered as one line per row in reports and in chat answers.
|
|
38
|
+
_VERB = {
|
|
39
|
+
"chat": "Chatted with",
|
|
40
|
+
"run": "Ran",
|
|
41
|
+
"run-ticket": "Ran",
|
|
42
|
+
"scheduled": "Ran (schedule)",
|
|
43
|
+
"ticket_label": "Ran (ticket trigger)",
|
|
44
|
+
"cli": "Ran (cli)",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def describe(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
49
|
+
detail: str = "") -> str:
|
|
50
|
+
"""The human-readable ``activity`` string stored in ``agent_activities``."""
|
|
51
|
+
res = result or {}
|
|
52
|
+
verb = _VERB.get(kind, "Ran")
|
|
53
|
+
status = "ok" if res.get("ok") else "error"
|
|
54
|
+
line = f"{verb} agent '{name}' — {status}"
|
|
55
|
+
if duration_s:
|
|
56
|
+
line += f" ({round(float(duration_s), 1)}s)"
|
|
57
|
+
err = str(res.get("error") or "").strip()
|
|
58
|
+
if status == "error" and err:
|
|
59
|
+
line += f": {err[:200]}"
|
|
60
|
+
if detail:
|
|
61
|
+
line += f" — {detail}"
|
|
62
|
+
return line
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def emit(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
66
|
+
*, client: "BackendClient | None" = None, detail: str = "") -> bool:
|
|
67
|
+
"""Emit ONE ``{activity, agentId}`` event for a finished agent run.
|
|
68
|
+
|
|
69
|
+
Returns True when the webhook accepted it, False otherwise (unknown agent,
|
|
70
|
+
no token, backend down) — never raises.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
if client is None:
|
|
74
|
+
from sdk.client import BackendClient
|
|
75
|
+
client = BackendClient.from_config()
|
|
76
|
+
agent_id = await client.resolve_agent_id(name)
|
|
77
|
+
if not agent_id: # local-only agent → not tracked
|
|
78
|
+
return False
|
|
79
|
+
return await client.log_agent_activity(
|
|
80
|
+
describe(name, kind, result, duration_s, detail), agent_id,
|
|
81
|
+
)
|
|
82
|
+
except Exception as exc: # noqa: BLE001 — activity emit must never break a run
|
|
83
|
+
logger.debug("activity emit failed agent=%s err=%s", name, exc)
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
async def record_run(name: str, kind: str, result: dict | None, duration_s: float = 0.0,
|
|
88
|
+
*, client: "BackendClient | None" = None, detail: str = "") -> None:
|
|
89
|
+
"""Record a finished run to BOTH sinks: local ledger, then platform feed.
|
|
90
|
+
|
|
91
|
+
The ledger write happens first and independently of the webhook, so a
|
|
92
|
+
backend outage still leaves per-agent utilisation on the VPS (which is what
|
|
93
|
+
``/api/ui/agents/usage`` and the report's "VPS Agent Logs" read).
|
|
94
|
+
"""
|
|
95
|
+
res: dict[str, Any] = result or {}
|
|
96
|
+
try:
|
|
97
|
+
from sdk.agents import usage as usage_mod
|
|
98
|
+
usage_mod.record(
|
|
99
|
+
name,
|
|
100
|
+
kind=kind,
|
|
101
|
+
duration_s=duration_s,
|
|
102
|
+
input_tokens=res.get("input_tokens", 0) or 0,
|
|
103
|
+
output_tokens=res.get("output_tokens", 0) or 0,
|
|
104
|
+
cost_usd=res.get("cost_usd", 0.0) or 0.0,
|
|
105
|
+
ok=bool(res.get("ok")),
|
|
106
|
+
)
|
|
107
|
+
except Exception as exc: # noqa: BLE001 — ledger write must never break a run
|
|
108
|
+
logger.debug("activity ledger write failed agent=%s err=%s", name, exc)
|
|
109
|
+
|
|
110
|
+
await emit(name, kind, res, duration_s, client=client, detail=detail)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""On-demand "run an agent ON a Jira ticket" — the manual counterpart to the
|
|
2
|
+
``agentd`` ``ticket_label`` trigger.
|
|
3
|
+
|
|
4
|
+
The daemon fires an agent when a To-Do ticket carries its label; this module
|
|
5
|
+
lets a human pick an agent AND a ticket and run it now (from the UI, the API, or
|
|
6
|
+
the CLI). It reuses the exact same engine — ``agentd._dispatch_agent_run`` — so
|
|
7
|
+
the ticket's fields fill the agent's ``{{variables}}`` and the work is planned,
|
|
8
|
+
gated, executed locally, and reported back to the platform against that ticket.
|
|
9
|
+
|
|
10
|
+
Because a human initiated the run, ``auto_approve=True`` is passed: the GO-AHEAD
|
|
11
|
+
is sent immediately, skipping the agent's ``approval.policy`` WhatsApp gate for
|
|
12
|
+
this single run.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from sdk.agents.manifest import AgentManifest
|
|
20
|
+
from sdk.client import BackendClient
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def resolve_manifest(
|
|
24
|
+
client: "BackendClient", name: str, scope: str = "custom", department_id: str = "",
|
|
25
|
+
) -> "AgentManifest | None":
|
|
26
|
+
"""Find the agent manifest to run, by ``scope``.
|
|
27
|
+
|
|
28
|
+
* ``custom`` (default) — a personal agent from the local manifests
|
|
29
|
+
(``~/.agentos/agents``).
|
|
30
|
+
* ``department`` — an ACTIVE department default pulled from the backend
|
|
31
|
+
(``list_department_agents``), materialized read-only (never written back),
|
|
32
|
+
exactly as ``agentd._load_department_agents`` does.
|
|
33
|
+
|
|
34
|
+
Returns None when no matching agent is found.
|
|
35
|
+
"""
|
|
36
|
+
from sdk.agents.manifest import AgentManifest, load_agents
|
|
37
|
+
|
|
38
|
+
name = (name or "").strip()
|
|
39
|
+
if scope == "department":
|
|
40
|
+
try:
|
|
41
|
+
rows = await client.list_department_agents()
|
|
42
|
+
except Exception: # noqa: BLE001 — treat a backend hiccup as "not found"
|
|
43
|
+
return None
|
|
44
|
+
for row in rows:
|
|
45
|
+
if row.get("name") == name and row.get("status") == "active":
|
|
46
|
+
try:
|
|
47
|
+
return AgentManifest(**row["manifest"])
|
|
48
|
+
except Exception: # noqa: BLE001 — invalid manifest → not runnable
|
|
49
|
+
return None
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
for m in load_agents():
|
|
53
|
+
if m.name == name:
|
|
54
|
+
return m
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
async def run_on_ticket(
|
|
59
|
+
client: "BackendClient", name: str, ticket_key: str = "",
|
|
60
|
+
scope: str = "custom", department_id: str = "",
|
|
61
|
+
variables: dict | None = None, secrets: dict | None = None,
|
|
62
|
+
) -> dict:
|
|
63
|
+
"""Run agent ``name`` (``scope``) now, OPTIONALLY against a Jira ticket.
|
|
64
|
+
|
|
65
|
+
* ``ticket_key`` — optional. When given, the ticket record is fetched (any
|
|
66
|
+
board status — a targeted run may re-run a Done ticket) and its fields fill
|
|
67
|
+
the agent's ``{{variables}}``. When omitted, the agent runs without a ticket.
|
|
68
|
+
* ``variables`` — optional EPHEMERAL ``{{var}}`` values for this run; they win
|
|
69
|
+
over ticket-derived values and are never persisted.
|
|
70
|
+
* ``secrets`` — optional EPHEMERAL env secrets for this run only; injected for
|
|
71
|
+
the engine invocation and never written to the agent's stored keys.
|
|
72
|
+
|
|
73
|
+
Resolves the manifest and dispatches through the shared pipeline with
|
|
74
|
+
``auto_approve=True``. Returns the dispatch outcome dict
|
|
75
|
+
(``{status, task_id, result, ok, error}``).
|
|
76
|
+
|
|
77
|
+
Raises ``ValueError`` when the agent — or an explicitly-named ticket — cannot
|
|
78
|
+
be found, so the caller reports a clear message.
|
|
79
|
+
"""
|
|
80
|
+
from sdk.agentd import _dispatch_agent_run
|
|
81
|
+
|
|
82
|
+
key = (ticket_key or "").strip()
|
|
83
|
+
task: dict | None = None
|
|
84
|
+
if key:
|
|
85
|
+
records = await client.list_tasks_by_keys([key])
|
|
86
|
+
if not records:
|
|
87
|
+
raise ValueError(f"Jira ticket '{key}' was not found.")
|
|
88
|
+
task = records[0]
|
|
89
|
+
|
|
90
|
+
manifest = await resolve_manifest(client, name, scope=scope, department_id=department_id)
|
|
91
|
+
if manifest is None:
|
|
92
|
+
raise ValueError(f"Agent '{name}' was not found (scope={scope}).")
|
|
93
|
+
|
|
94
|
+
return await _dispatch_agent_run(
|
|
95
|
+
manifest, client, task, auto_approve=True,
|
|
96
|
+
extra_variables=variables or None, extra_secrets=secrets or None,
|
|
97
|
+
)
|
|
@@ -131,6 +131,9 @@ def _agent_command(args: list[str]) -> None:
|
|
|
131
131
|
if verb == "run" and rest:
|
|
132
132
|
_agent_run(rest[0])
|
|
133
133
|
return
|
|
134
|
+
if verb == "run-ticket" and rest:
|
|
135
|
+
_agent_run_ticket(rest[0], rest[1:])
|
|
136
|
+
return
|
|
134
137
|
if verb == "chat" and len(rest) >= 2:
|
|
135
138
|
_agent_chat(rest[0], " ".join(rest[1:]))
|
|
136
139
|
return
|
|
@@ -156,17 +159,29 @@ def _agent_command(args: list[str]) -> None:
|
|
|
156
159
|
_agent_rm_key(rest[0], rest[1:])
|
|
157
160
|
return
|
|
158
161
|
|
|
159
|
-
print("usage: agentos agent <new|add PATH|list|run NAME|
|
|
160
|
-
"
|
|
161
|
-
"rm-key NAME KEY
|
|
162
|
+
print("usage: agentos agent <new|add PATH|list|run NAME|run-ticket NAME [--ticket KEY] "
|
|
163
|
+
"[--var k=v]… [--secret K=V]… [--scope department --department-id UUID]|chat NAME MESSAGE|"
|
|
164
|
+
"template [NAME]|logs NAME|enable NAME|disable NAME|rm NAME|set-key NAME KEY[=VALUE]…|"
|
|
165
|
+
"keys NAME|rm-key NAME KEY…>")
|
|
162
166
|
sys.exit(1)
|
|
163
167
|
|
|
164
168
|
|
|
165
169
|
def _agent_chat(name: str, message: str) -> None:
|
|
170
|
+
import time as _time
|
|
171
|
+
|
|
172
|
+
from sdk.agents import activity
|
|
166
173
|
from sdk.agents.chat import chat_with_agent
|
|
167
174
|
|
|
175
|
+
async def _run() -> dict:
|
|
176
|
+
started = _time.monotonic()
|
|
177
|
+
res = await chat_with_agent(name, message)
|
|
178
|
+
# Same two sinks as the control panel: local usage ledger + the platform
|
|
179
|
+
# `agent_activities` feed. A CLI chat is as much an agent run as a UI one.
|
|
180
|
+
await activity.record_run(name, "chat", res, _time.monotonic() - started)
|
|
181
|
+
return res
|
|
182
|
+
|
|
168
183
|
try:
|
|
169
|
-
result = asyncio.run(
|
|
184
|
+
result = asyncio.run(_run())
|
|
170
185
|
except LookupError as exc:
|
|
171
186
|
print(f"error: {exc}")
|
|
172
187
|
sys.exit(1)
|
|
@@ -220,6 +235,61 @@ def _agent_list() -> None:
|
|
|
220
235
|
)
|
|
221
236
|
|
|
222
237
|
|
|
238
|
+
def _agent_run_ticket(name: str, flags: list[str]) -> None:
|
|
239
|
+
"""`agentos agent run-ticket NAME [--ticket KEY] [--var k=v]… [--secret K=V]…
|
|
240
|
+
[--scope department] [--department-id UUID]`.
|
|
241
|
+
|
|
242
|
+
Runs the agent through the platform pipeline. A ``--ticket`` is OPTIONAL (its
|
|
243
|
+
fields fill the agent's ``{{variables}}``); ``--var``/``--secret`` are
|
|
244
|
+
EPHEMERAL (this run only, never stored). Auto-approved.
|
|
245
|
+
"""
|
|
246
|
+
from sdk.agents import ticket_run
|
|
247
|
+
from sdk.client import BackendClient
|
|
248
|
+
|
|
249
|
+
scope, department_id, ticket_key = "custom", "", ""
|
|
250
|
+
variables: dict = {}
|
|
251
|
+
secrets: dict = {}
|
|
252
|
+
i = 0
|
|
253
|
+
while i < len(flags):
|
|
254
|
+
f = flags[i]
|
|
255
|
+
if f == "--ticket" and i + 1 < len(flags):
|
|
256
|
+
ticket_key = flags[i + 1]; i += 2
|
|
257
|
+
elif f == "--scope" and i + 1 < len(flags):
|
|
258
|
+
scope = flags[i + 1]; i += 2
|
|
259
|
+
elif f == "--department-id" and i + 1 < len(flags):
|
|
260
|
+
department_id = flags[i + 1]; i += 2
|
|
261
|
+
elif f == "--var" and i + 1 < len(flags):
|
|
262
|
+
k, _, v = flags[i + 1].partition("=")
|
|
263
|
+
if k.strip():
|
|
264
|
+
variables[k.strip()] = v
|
|
265
|
+
i += 2
|
|
266
|
+
elif f == "--secret" and i + 1 < len(flags):
|
|
267
|
+
k, _, v = flags[i + 1].partition("=")
|
|
268
|
+
if k.strip():
|
|
269
|
+
secrets[k.strip()] = v
|
|
270
|
+
i += 2
|
|
271
|
+
else:
|
|
272
|
+
i += 1
|
|
273
|
+
|
|
274
|
+
client = BackendClient.from_config()
|
|
275
|
+
try:
|
|
276
|
+
outcome = asyncio.run(ticket_run.run_on_ticket(
|
|
277
|
+
client, name, ticket_key, scope=scope, department_id=department_id,
|
|
278
|
+
variables=variables or None, secrets=secrets or None,
|
|
279
|
+
))
|
|
280
|
+
except ValueError as exc:
|
|
281
|
+
print(f"error: {exc}")
|
|
282
|
+
sys.exit(1)
|
|
283
|
+
|
|
284
|
+
if outcome.get("error"):
|
|
285
|
+
print(f"error: {outcome['error']}")
|
|
286
|
+
sys.exit(1)
|
|
287
|
+
target = f" on {ticket_key}" if ticket_key else ""
|
|
288
|
+
print(f"ran '{name}'{target} — status={outcome.get('status') or 'done'}")
|
|
289
|
+
if outcome.get("result"):
|
|
290
|
+
print(f"\n{outcome['result']}")
|
|
291
|
+
|
|
292
|
+
|
|
223
293
|
def _agent_run(name: str) -> None:
|
|
224
294
|
from sdk.agents.manifest import AgentManifest, load_agents
|
|
225
295
|
from sdk.client import BackendClient
|
|
@@ -1093,6 +1093,35 @@ class BackendClient:
|
|
|
1093
1093
|
_AGENT_ID_CACHE[nm] = "" # negative-cache: not a backend agent
|
|
1094
1094
|
return None
|
|
1095
1095
|
|
|
1096
|
+
async def report_vps_allocation(self, payload: dict) -> bool:
|
|
1097
|
+
"""PATCH ``{ML_GATEWAY_URL}/ml-api/contabo/allocation`` — this machine's health.
|
|
1098
|
+
|
|
1099
|
+
The allocation row is resolved SERVER-side from the ``co_`` token (one row
|
|
1100
|
+
per user — ``contabo_allocation``, docs/DATABASE_SCHEMA.md #18), so the body
|
|
1101
|
+
carries metrics only: ``status``, ``cpuUsage``, ``ramUsage``, ``usedRam``,
|
|
1102
|
+
``totalRam``, ``diskUsage``, ``totalDisk``, ``bandwidthUsed``, ``ipAddress``,
|
|
1103
|
+
``uptimeSeconds``, ``lastPingedAt``.
|
|
1104
|
+
|
|
1105
|
+
Driven by the SDK-side health cron (``sdk.vps_health``) — deliberately NOT by
|
|
1106
|
+
the AI backend, so VPS liveness reporting does not depend on that service.
|
|
1107
|
+
Best-effort: never raises; a failed ping is logged and the next tick retries.
|
|
1108
|
+
"""
|
|
1109
|
+
from sdk.config import ML_GATEWAY_URL
|
|
1110
|
+
|
|
1111
|
+
url = f"{ML_GATEWAY_URL.rstrip('/')}/ml-api/contabo/allocation"
|
|
1112
|
+
headers = {**self._headers, "accept": "*/*", "developer-name": "null", "product": ""}
|
|
1113
|
+
try:
|
|
1114
|
+
async with httpx.AsyncClient(timeout=20.0) as client:
|
|
1115
|
+
resp = await client.patch(url, json=payload, headers=headers)
|
|
1116
|
+
if resp.status_code >= 400:
|
|
1117
|
+
logger.warning("client.report_vps_allocation failed status=%s body=%s",
|
|
1118
|
+
resp.status_code, resp.text[:200])
|
|
1119
|
+
return False
|
|
1120
|
+
return True
|
|
1121
|
+
except Exception as exc: # noqa: BLE001
|
|
1122
|
+
logger.warning("client.report_vps_allocation error err=%s", exc)
|
|
1123
|
+
return False
|
|
1124
|
+
|
|
1096
1125
|
async def notify_whatsapp(
|
|
1097
1126
|
self,
|
|
1098
1127
|
task_id: str,
|