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.
Files changed (105) hide show
  1. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/PKG-INFO +2 -1
  2. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/PKG-INFO +2 -1
  3. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/SOURCES.txt +10 -1
  4. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/requires.txt +1 -0
  5. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/pyproject.toml +5 -0
  6. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/__init__.py +2 -2
  7. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agentd.py +72 -22
  8. devops_bot_sdk-1.4.159/sdk/agents/activity.py +110 -0
  9. devops_bot_sdk-1.4.159/sdk/agents/ticket_run.py +97 -0
  10. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/cli.py +74 -4
  11. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/client.py +29 -0
  12. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/config.py +19 -0
  13. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/api.py +148 -15
  14. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/ip_allowlist.py +72 -8
  15. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/models.py +53 -0
  16. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/server.py +40 -1
  17. devops_bot_sdk-1.4.159/sdk/ui/session.py +76 -0
  18. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/agents.html +16 -0
  19. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/app.js +94 -0
  20. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/custom-agents.html +11 -0
  21. devops_bot_sdk-1.4.159/sdk/ui/static/login.html +111 -0
  22. devops_bot_sdk-1.4.159/sdk/vps_health.py +264 -0
  23. devops_bot_sdk-1.4.159/tests/test_agent_activity.py +93 -0
  24. devops_bot_sdk-1.4.159/tests/test_agent_run_ticket.py +222 -0
  25. devops_bot_sdk-1.4.159/tests/test_otp_login.py +178 -0
  26. devops_bot_sdk-1.4.159/tests/test_vps_health.py +209 -0
  27. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/README.md +0 -0
  28. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  29. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  30. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  31. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/__init__.py +0 -0
  32. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/builder.py +0 -0
  33. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/chat.py +0 -0
  34. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/connectors.py +0 -0
  35. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/__init__.py +0 -0
  36. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/claude_code.py +0 -0
  37. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/http.py +0 -0
  38. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/mcp.py +0 -0
  39. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/n8n.py +0 -0
  40. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/engines/shell.py +0 -0
  41. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/integrations.py +0 -0
  42. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/inventory.py +0 -0
  43. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/library/__init__.py +0 -0
  44. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/manifest.py +0 -0
  45. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/prompt_studio.py +0 -0
  46. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/prompt_template.py +0 -0
  47. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/prompt_vars.py +0 -0
  48. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/run_log.py +0 -0
  49. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/secrets.py +0 -0
  50. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/usage.py +0 -0
  51. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/agents/varstore.py +0 -0
  52. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/collectors/__init__.py +0 -0
  53. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/collectors/files.py +0 -0
  54. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/collectors/process.py +0 -0
  55. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/collectors/screenshot.py +0 -0
  56. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/crucial.py +0 -0
  57. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/__init__.py +0 -0
  58. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/assets.py +0 -0
  59. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/bootstrap.py +0 -0
  60. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/browser.py +0 -0
  61. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/compare.py +0 -0
  62. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/design_verify/loop.py +0 -0
  63. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/exceptions.py +0 -0
  64. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/git_ops.py +0 -0
  65. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/graphify.py +0 -0
  66. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/hooks/__init__.py +0 -0
  67. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/hooks/crucial_guard.py +0 -0
  68. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ipc/__init__.py +0 -0
  69. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ipc/electron_bridge.py +0 -0
  70. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ipc/handlers.py +0 -0
  71. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/local_exec.py +0 -0
  72. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/models/__init__.py +0 -0
  73. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/models/envelope.py +0 -0
  74. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/models/requests.py +0 -0
  75. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/models/responses.py +0 -0
  76. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/models/snapshots.py +0 -0
  77. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/platform_compat.py +0 -0
  78. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/py.typed +0 -0
  79. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/run_auto.py +0 -0
  80. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/sse.py +0 -0
  81. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/test.py +0 -0
  82. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/test_pipeline.py +0 -0
  83. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/__init__.py +0 -0
  84. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/dashboard.py +0 -0
  85. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/health.py +0 -0
  86. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/jobs.py +0 -0
  87. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/runner.py +0 -0
  88. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/security.py +0 -0
  89. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/service.py +0 -0
  90. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/setup_manager.py +0 -0
  91. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/default-agents.html +0 -0
  92. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/index.html +0 -0
  93. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/runs.html +0 -0
  94. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/setup.html +0 -0
  95. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/styles.css +0 -0
  96. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/ui/static/tickets.html +0 -0
  97. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/sdk/updater.py +0 -0
  98. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/setup.cfg +0 -0
  99. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_agent_chat_features.py +0 -0
  100. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_agent_prompt_async.py +0 -0
  101. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_agent_prompt_models.py +0 -0
  102. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_agents_connectors_builder.py +0 -0
  103. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_git_action.py +0 -0
  104. {devops_bot_sdk-1.4.153 → devops_bot_sdk-1.4.159}/tests/test_health.py +0 -0
  105. {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.153
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.153
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/test_updater_extras.py
101
+ tests/test_otp_login.py
102
+ tests/test_updater_extras.py
103
+ tests/test_vps_health.py
@@ -19,3 +19,4 @@ Pillow>=10.0
19
19
  [ui]
20
20
  fastapi>=0.110
21
21
  uvicorn>=0.29
22
+ psutil>=5.9
@@ -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.153
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.153" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
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
- ) -> None:
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
- - ``approval.policy == "never"``: agentd sends the GO-AHEAD itself
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
- secret_env = agent_secrets.resolved_env(manifest.name) if stored_secrets else None
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: refilled FRESH from THIS ticket every run (never
297
- # persisted). A scheduled/manual run with no ticket leaves them for the agent
298
- # to ask about or default, exactly as before.
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
- if task:
301
- names = extract_variables(manifest.prompt)
302
- if names:
303
- values = values_from_ticket(names, task)
304
- resolved_prompt, missing = substitute_variables(manifest.prompt, values)
305
- if values:
306
- logger.info(
307
- "agentd.vars_filled agent=%s filled=%s missing=%s",
308
- manifest.name, sorted(values), sorted(missing),
309
- )
310
- manifest = manifest.model_copy(update={"prompt": resolved_prompt})
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
- return
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 agent — agentd IS the approver.
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=stored_secrets or None,
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
- return
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|chat NAME MESSAGE|template [NAME]|"
160
- "logs NAME|enable NAME|disable NAME|rm NAME|set-key NAME KEY[=VALUE]…|keys NAME|"
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(chat_with_agent(name, message))
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,