devops-bot-sdk 1.4.151__tar.gz → 1.4.158__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 (101) hide show
  1. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/SOURCES.txt +6 -0
  4. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agentd.py +54 -22
  6. devops_bot_sdk-1.4.158/sdk/agents/ticket_run.py +97 -0
  7. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/cli.py +62 -3
  8. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/config.py +29 -1
  9. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/api.py +200 -67
  10. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/ip_allowlist.py +72 -8
  11. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/models.py +53 -0
  12. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/server.py +6 -0
  13. devops_bot_sdk-1.4.158/sdk/ui/session.py +76 -0
  14. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/agents.html +16 -0
  15. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/app.js +94 -0
  16. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/custom-agents.html +11 -0
  17. devops_bot_sdk-1.4.158/sdk/ui/static/login.html +111 -0
  18. devops_bot_sdk-1.4.158/tests/test_agent_prompt_async.py +74 -0
  19. devops_bot_sdk-1.4.158/tests/test_agent_run_ticket.py +222 -0
  20. devops_bot_sdk-1.4.158/tests/test_otp_login.py +170 -0
  21. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/README.md +0 -0
  22. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  23. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  24. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/requires.txt +0 -0
  25. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  26. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/pyproject.toml +0 -0
  27. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/builder.py +0 -0
  29. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/chat.py +0 -0
  30. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/connectors.py +0 -0
  31. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/__init__.py +0 -0
  32. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/claude_code.py +0 -0
  33. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/http.py +0 -0
  34. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/mcp.py +0 -0
  35. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/n8n.py +0 -0
  36. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/shell.py +0 -0
  37. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/integrations.py +0 -0
  38. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/inventory.py +0 -0
  39. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/library/__init__.py +0 -0
  40. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/manifest.py +0 -0
  41. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/prompt_studio.py +0 -0
  42. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/prompt_template.py +0 -0
  43. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/prompt_vars.py +0 -0
  44. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/run_log.py +0 -0
  45. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/secrets.py +0 -0
  46. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/usage.py +0 -0
  47. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/varstore.py +0 -0
  48. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/client.py +0 -0
  49. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/collectors/__init__.py +0 -0
  50. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/collectors/files.py +0 -0
  51. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/collectors/process.py +0 -0
  52. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/collectors/screenshot.py +0 -0
  53. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/crucial.py +0 -0
  54. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/__init__.py +0 -0
  55. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/assets.py +0 -0
  56. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/bootstrap.py +0 -0
  57. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/browser.py +0 -0
  58. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/compare.py +0 -0
  59. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/loop.py +0 -0
  60. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/exceptions.py +0 -0
  61. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/git_ops.py +0 -0
  62. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/graphify.py +0 -0
  63. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/hooks/__init__.py +0 -0
  64. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/hooks/crucial_guard.py +0 -0
  65. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ipc/__init__.py +0 -0
  66. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ipc/electron_bridge.py +0 -0
  67. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ipc/handlers.py +0 -0
  68. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/local_exec.py +0 -0
  69. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/models/__init__.py +0 -0
  70. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/models/envelope.py +0 -0
  71. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/models/requests.py +0 -0
  72. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/models/responses.py +0 -0
  73. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/models/snapshots.py +0 -0
  74. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/platform_compat.py +0 -0
  75. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/py.typed +0 -0
  76. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/run_auto.py +0 -0
  77. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/sse.py +0 -0
  78. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/test.py +0 -0
  79. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/test_pipeline.py +0 -0
  80. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/__init__.py +0 -0
  81. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/dashboard.py +0 -0
  82. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/health.py +0 -0
  83. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/jobs.py +0 -0
  84. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/runner.py +0 -0
  85. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/security.py +0 -0
  86. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/service.py +0 -0
  87. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/setup_manager.py +0 -0
  88. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/default-agents.html +0 -0
  89. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/index.html +0 -0
  90. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/runs.html +0 -0
  91. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/setup.html +0 -0
  92. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/styles.css +0 -0
  93. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/tickets.html +0 -0
  94. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/updater.py +0 -0
  95. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/setup.cfg +0 -0
  96. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/tests/test_agent_chat_features.py +0 -0
  97. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/tests/test_agent_prompt_models.py +0 -0
  98. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/tests/test_agents_connectors_builder.py +0 -0
  99. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/tests/test_git_action.py +0 -0
  100. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/tests/test_health.py +0 -0
  101. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/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.151
3
+ Version: 1.4.158
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.151
3
+ Version: 1.4.158
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
@@ -35,6 +35,7 @@ sdk/agents/prompt_template.py
35
35
  sdk/agents/prompt_vars.py
36
36
  sdk/agents/run_log.py
37
37
  sdk/agents/secrets.py
38
+ sdk/agents/ticket_run.py
38
39
  sdk/agents/usage.py
39
40
  sdk/agents/varstore.py
40
41
  sdk/agents/engines/__init__.py
@@ -75,19 +76,24 @@ sdk/ui/runner.py
75
76
  sdk/ui/security.py
76
77
  sdk/ui/server.py
77
78
  sdk/ui/service.py
79
+ sdk/ui/session.py
78
80
  sdk/ui/setup_manager.py
79
81
  sdk/ui/static/agents.html
80
82
  sdk/ui/static/app.js
81
83
  sdk/ui/static/custom-agents.html
82
84
  sdk/ui/static/default-agents.html
83
85
  sdk/ui/static/index.html
86
+ sdk/ui/static/login.html
84
87
  sdk/ui/static/runs.html
85
88
  sdk/ui/static/setup.html
86
89
  sdk/ui/static/styles.css
87
90
  sdk/ui/static/tickets.html
88
91
  tests/test_agent_chat_features.py
92
+ tests/test_agent_prompt_async.py
89
93
  tests/test_agent_prompt_models.py
94
+ tests/test_agent_run_ticket.py
90
95
  tests/test_agents_connectors_builder.py
91
96
  tests/test_git_action.py
92
97
  tests/test_health.py
98
+ tests/test_otp_login.py
93
99
  tests/test_updater_extras.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.151
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.151" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
33
+ __version__ = "1.4.158" # 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
@@ -251,7 +251,9 @@ async def _wait_for_human_approval(client: BackendClient, task_id: str) -> str:
251
251
 
252
252
  async def _dispatch_agent_run(
253
253
  manifest: "AgentManifest", client: BackendClient, task: dict | None = None,
254
- ) -> None:
254
+ *, auto_approve: bool = False,
255
+ extra_variables: dict | None = None, extra_secrets: dict | None = None,
256
+ ) -> dict:
255
257
  """Run ONE loaded ``AgentManifest`` end to end, for one trigger firing.
256
258
 
257
259
  1. Resolve skills + (optionally) an MCP config from the manifest.
@@ -262,7 +264,8 @@ async def _dispatch_agent_run(
262
264
  run, but pauses at ``awaiting_approval`` instead of executing, exactly
263
265
  like the desktop's local-execution hybrid for Jira coding tasks.
264
266
  4. Drive the GO-AHEAD / FINALIZE round-trip:
265
- - ``approval.policy == "never"``: agentd sends the GO-AHEAD itself
267
+ - ``auto_approve`` (a human-initiated on-demand run) OR
268
+ ``approval.policy == "never"``: agentd sends the GO-AHEAD itself
266
269
  (``approval_reply(..., decision="APPROVED", local_execution_result=None)``)
267
270
  the moment the run reaches ``awaiting_approval``.
268
271
  - otherwise: wait for a human's WhatsApp reply to land (no early
@@ -271,7 +274,15 @@ async def _dispatch_agent_run(
271
274
  ``sdk.agents.engines.claude_code.run`` and report the result back via
272
275
  a second ``approval_reply(..., local_execution_result={...})`` call,
273
276
  which finalizes the run server-side.
277
+
278
+ ``auto_approve=True`` is used by the on-demand "run agent on a ticket" path
279
+ (UI / API / CLI), where a human already clicked Run — so the agent's
280
+ ``approval.policy`` WhatsApp gate is skipped for this single run.
281
+
282
+ Returns an outcome dict — ``{status, task_id, result, ok, error}`` — for the
283
+ on-demand callers; the daemon's trigger loops ignore the return value.
274
284
  """
285
+ outcome: dict = {"status": "", "task_id": "", "result": "", "ok": False, "error": ""}
275
286
  from sdk.agents import integrations as integrations_mod
276
287
  from sdk.agents import manifest as manifest_mod
277
288
  from sdk.agents import secrets as agent_secrets
@@ -284,7 +295,15 @@ async def _dispatch_agent_run(
284
295
  # only when the owner edits the agent. Empty for department agents, whose keys
285
296
  # come from the host .env already in os.environ.
286
297
  stored_secrets = agent_secrets.load_secrets(manifest.name)
287
- secret_env = agent_secrets.resolved_env(manifest.name) if stored_secrets else None
298
+ # EPHEMERAL secrets: supplied for THIS run only (UI/API/CLI), merged on top of
299
+ # any stored keys and injected as env — NEVER written back to disk, so they
300
+ # vanish when the run ends. Explicit run-time values win over stored ones.
301
+ effective_secrets = {**(stored_secrets or {}), **(extra_secrets or {})}
302
+ if extra_secrets:
303
+ import os as _os
304
+ secret_env = {**_os.environ, **effective_secrets}
305
+ else:
306
+ secret_env = agent_secrets.resolved_env(manifest.name) if stored_secrets else None
288
307
 
289
308
  mcp_config: dict[str, Any] | None = None
290
309
  if manifest.integrations:
@@ -293,21 +312,23 @@ async def _dispatch_agent_run(
293
312
  manifest.integrations, available, env=secret_env,
294
313
  )
295
314
 
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.
315
+ # EPHEMERAL variables: filled FRESH from the ticket (when one is attached) and/or
316
+ # from explicit run-time values — never persisted. Explicit ``extra_variables``
317
+ # win over ticket-derived ones. With neither, unresolved {{vars}} are left for
318
+ # the agent to ask about or default, exactly as before.
299
319
  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})
320
+ names = extract_variables(manifest.prompt)
321
+ if names and (task or extra_variables):
322
+ values = values_from_ticket(names, task) if task else {}
323
+ if extra_variables:
324
+ values = {**values, **{k: str(v) for k, v in extra_variables.items() if str(v).strip()}}
325
+ resolved_prompt, missing = substitute_variables(manifest.prompt, values)
326
+ if values:
327
+ logger.info(
328
+ "agentd.vars_filled agent=%s filled=%s missing=%s",
329
+ manifest.name, sorted(values), sorted(missing),
330
+ )
331
+ manifest = manifest.model_copy(update={"prompt": resolved_prompt})
311
332
 
312
333
  if manifest.engine != "claude_code":
313
334
  # Only claude_code is wired end-to-end today (sdk/agents/engines/{mcp,
@@ -317,7 +338,8 @@ async def _dispatch_agent_run(
317
338
  logger.warning(
318
339
  "agentd.engine_not_wired engine=%s agent=%s", manifest.engine, manifest.name,
319
340
  )
320
- return
341
+ outcome["error"] = f"engine '{manifest.engine}' is not wired (only claude_code)."
342
+ return outcome
321
343
 
322
344
  prompt_text = manifest.prompt
323
345
  if skill_text:
@@ -341,8 +363,9 @@ async def _dispatch_agent_run(
341
363
  return
342
364
  handled.add(task_id)
343
365
 
344
- if policy == "never":
345
- # No human in the loop for this agent — agentd IS the approver.
366
+ if auto_approve or policy == "never":
367
+ # No human in the loop for this run — agentd IS the approver
368
+ # (agent policy 'never', or a human already clicked Run on-demand).
346
369
  try:
347
370
  await client.approval_reply(
348
371
  task_id, decision="APPROVED", local_execution_result=None,
@@ -380,15 +403,21 @@ async def _dispatch_agent_run(
380
403
  run_log.log_event(_log_path, ev)
381
404
 
382
405
  local_result = await claude_code_engine.run(
383
- manifest, skill_text, mcp_config, extra_env=stored_secrets or None,
406
+ manifest, skill_text, mcp_config, extra_env=effective_secrets or None,
384
407
  on_event=_tee,
385
408
  )
386
409
  run_log.finish_run(_log_path, local_result)
410
+ outcome.update({
411
+ "task_id": task_id,
412
+ "result": local_result.get("result", ""),
413
+ "ok": bool(local_result.get("ok", True)),
414
+ })
387
415
  except Exception as exc: # noqa: BLE001
388
416
  logger.warning(
389
417
  "agentd.local_run_failed agent=%s task=%s err=%s",
390
418
  manifest.name, task_id, exc,
391
419
  )
420
+ outcome["error"] = str(exc)
392
421
  return
393
422
 
394
423
  # FINALIZE — same field names sdk.local_exec.run_claude_local's result
@@ -417,11 +446,14 @@ async def _dispatch_agent_run(
417
446
  result = await client.orchestrate(req, on_status_update=_on_status_update)
418
447
  except Exception as exc: # noqa: BLE001
419
448
  logger.warning("agentd.orchestrate_failed agent=%s err=%s", manifest.name, exc)
420
- return
449
+ outcome["error"] = str(exc)
450
+ return outcome
421
451
 
422
452
  logger.info(
423
453
  "agentd.run_complete agent=%s status=%s", manifest.name, getattr(result, "status", ""),
424
454
  )
455
+ outcome["status"] = getattr(result, "status", "") or ""
456
+ return outcome
425
457
 
426
458
 
427
459
  # ── The daemon ───────────────────────────────────────────────────────────────
@@ -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,9 +159,10 @@ 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
 
@@ -220,6 +224,61 @@ def _agent_list() -> None:
220
224
  )
221
225
 
222
226
 
227
+ def _agent_run_ticket(name: str, flags: list[str]) -> None:
228
+ """`agentos agent run-ticket NAME [--ticket KEY] [--var k=v]… [--secret K=V]…
229
+ [--scope department] [--department-id UUID]`.
230
+
231
+ Runs the agent through the platform pipeline. A ``--ticket`` is OPTIONAL (its
232
+ fields fill the agent's ``{{variables}}``); ``--var``/``--secret`` are
233
+ EPHEMERAL (this run only, never stored). Auto-approved.
234
+ """
235
+ from sdk.agents import ticket_run
236
+ from sdk.client import BackendClient
237
+
238
+ scope, department_id, ticket_key = "custom", "", ""
239
+ variables: dict = {}
240
+ secrets: dict = {}
241
+ i = 0
242
+ while i < len(flags):
243
+ f = flags[i]
244
+ if f == "--ticket" and i + 1 < len(flags):
245
+ ticket_key = flags[i + 1]; i += 2
246
+ elif f == "--scope" and i + 1 < len(flags):
247
+ scope = flags[i + 1]; i += 2
248
+ elif f == "--department-id" and i + 1 < len(flags):
249
+ department_id = flags[i + 1]; i += 2
250
+ elif f == "--var" and i + 1 < len(flags):
251
+ k, _, v = flags[i + 1].partition("=")
252
+ if k.strip():
253
+ variables[k.strip()] = v
254
+ i += 2
255
+ elif f == "--secret" and i + 1 < len(flags):
256
+ k, _, v = flags[i + 1].partition("=")
257
+ if k.strip():
258
+ secrets[k.strip()] = v
259
+ i += 2
260
+ else:
261
+ i += 1
262
+
263
+ client = BackendClient.from_config()
264
+ try:
265
+ outcome = asyncio.run(ticket_run.run_on_ticket(
266
+ client, name, ticket_key, scope=scope, department_id=department_id,
267
+ variables=variables or None, secrets=secrets or None,
268
+ ))
269
+ except ValueError as exc:
270
+ print(f"error: {exc}")
271
+ sys.exit(1)
272
+
273
+ if outcome.get("error"):
274
+ print(f"error: {outcome['error']}")
275
+ sys.exit(1)
276
+ target = f" on {ticket_key}" if ticket_key else ""
277
+ print(f"ran '{name}'{target} — status={outcome.get('status') or 'done'}")
278
+ if outcome.get("result"):
279
+ print(f"\n{outcome['result']}")
280
+
281
+
223
282
  def _agent_run(name: str) -> None:
224
283
  from sdk.agents.manifest import AgentManifest, load_agents
225
284
  from sdk.client import BackendClient
@@ -149,6 +149,13 @@ def _dotenv_value(key: str) -> str:
149
149
  return ""
150
150
 
151
151
 
152
+ # Built-in permanent allowlist — the central AI backend's IP(s). Baked in so the
153
+ # backend can reach EVERY VPS's /api/ui/* out of the box (no per-VPS .env needed);
154
+ # always allowed, treated as permanent (not removable via the API). Extend/override
155
+ # with AGENTOS_UI_ALLOWED_IPS in .env or the process env.
156
+ _DEFAULT_ALLOWED_IPS: list[str] = ["20.12.201.148"]
157
+
158
+
152
159
  def _parse_ip_list(raw: str) -> list[str]:
153
160
  """Split an IP allowlist string, tolerant of JSON-ish list syntax.
154
161
 
@@ -181,6 +188,7 @@ def get_allowed_ips() -> list[str]:
181
188
  change. CIDR/prefix entries are supported by the middleware.
182
189
  """
183
190
  sources = [
191
+ ",".join(_DEFAULT_ALLOWED_IPS), # built-in backend IP(s) — always allowed
184
192
  _dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
185
193
  os.environ.get("AGENTOS_UI_ALLOWED_IPS", ""),
186
194
  _load_raw().get("allowed_ips", ""),
@@ -205,7 +213,8 @@ def permanent_allowed_ips() -> list[str]:
205
213
  through the API — change them by editing ``.env`` and restarting.
206
214
  """
207
215
  out: list[str] = []
208
- for src in (_dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
216
+ for src in (",".join(_DEFAULT_ALLOWED_IPS), # built-in backend IP(s) — always permanent
217
+ _dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
209
218
  os.environ.get("AGENTOS_UI_ALLOWED_IPS", "")):
210
219
  for ip in _parse_ip_list(src):
211
220
  if ip not in out:
@@ -278,6 +287,25 @@ def get_branch_name_template() -> str | None:
278
287
  return val.strip() if val and val.strip() else None
279
288
 
280
289
 
290
+ def get_session_secret() -> str:
291
+ """Return the per-SDK secret used to sign OTP login sessions.
292
+
293
+ Generated once (32 random bytes, hex) on first use and persisted to
294
+ config.toml ``session_secret`` — so sessions survive a restart but are unique
295
+ per box (a session minted on one VPS is meaningless on another). The merge
296
+ preserves ``token_enc`` / ``base_url`` / ``allowed_ips``. File mode stays 0600.
297
+ """
298
+ import secrets as _secrets
299
+
300
+ raw = _load_raw()
301
+ secret = (raw.get("session_secret") or "").strip()
302
+ if not secret:
303
+ secret = _secrets.token_hex(32)
304
+ raw["session_secret"] = secret
305
+ _save_raw(raw)
306
+ return secret
307
+
308
+
281
309
  def get_token() -> str:
282
310
  """Return the stored apiTokenHash. Raises TokenNotConfigured if missing."""
283
311
  cfg = load()