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.
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/SOURCES.txt +6 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/__init__.py +2 -2
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agentd.py +54 -22
- devops_bot_sdk-1.4.158/sdk/agents/ticket_run.py +97 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/cli.py +62 -3
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/config.py +29 -1
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/api.py +200 -67
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/ip_allowlist.py +72 -8
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/models.py +53 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/server.py +6 -0
- devops_bot_sdk-1.4.158/sdk/ui/session.py +76 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/agents.html +16 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/app.js +94 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/custom-agents.html +11 -0
- devops_bot_sdk-1.4.158/sdk/ui/static/login.html +111 -0
- devops_bot_sdk-1.4.158/tests/test_agent_prompt_async.py +74 -0
- devops_bot_sdk-1.4.158/tests/test_agent_run_ticket.py +222 -0
- devops_bot_sdk-1.4.158/tests/test_otp_login.py +170 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/README.md +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/pyproject.toml +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/prompt_studio.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/client.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/health.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/security.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/setup.cfg +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/tests/test_agent_prompt_models.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/tests/test_agents_connectors_builder.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/tests/test_git_action.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/tests/test_health.py +0 -0
- {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.158}/tests/test_updater_extras.py +0 -0
|
@@ -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.
|
|
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.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
|
-
|
|
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
|
-
- ``
|
|
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
|
-
|
|
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:
|
|
297
|
-
#
|
|
298
|
-
#
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
if
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
310
|
-
|
|
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
|
-
|
|
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
|
|
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=
|
|
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
|
-
|
|
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|
|
|
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
|
|
|
@@ -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 (
|
|
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()
|