devops-bot-sdk 1.4.151__tar.gz → 1.4.153__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 (96) hide show
  1. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
  4. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/config.py +10 -1
  6. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/api.py +86 -67
  7. devops_bot_sdk-1.4.153/tests/test_agent_prompt_async.py +74 -0
  8. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/README.md +0 -0
  9. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  10. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  11. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/requires.txt +0 -0
  12. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  13. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/pyproject.toml +0 -0
  14. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agentd.py +0 -0
  15. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/__init__.py +0 -0
  16. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/builder.py +0 -0
  17. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/chat.py +0 -0
  18. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/connectors.py +0 -0
  19. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/engines/__init__.py +0 -0
  20. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/engines/claude_code.py +0 -0
  21. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/engines/http.py +0 -0
  22. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/engines/mcp.py +0 -0
  23. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/engines/n8n.py +0 -0
  24. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/engines/shell.py +0 -0
  25. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/integrations.py +0 -0
  26. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/inventory.py +0 -0
  27. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/library/__init__.py +0 -0
  28. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/manifest.py +0 -0
  29. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/prompt_studio.py +0 -0
  30. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/prompt_template.py +0 -0
  31. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/prompt_vars.py +0 -0
  32. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/run_log.py +0 -0
  33. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/secrets.py +0 -0
  34. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/usage.py +0 -0
  35. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/agents/varstore.py +0 -0
  36. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/cli.py +0 -0
  37. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/client.py +0 -0
  38. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/collectors/__init__.py +0 -0
  39. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/collectors/files.py +0 -0
  40. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/collectors/process.py +0 -0
  41. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/collectors/screenshot.py +0 -0
  42. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/crucial.py +0 -0
  43. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/design_verify/__init__.py +0 -0
  44. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/design_verify/assets.py +0 -0
  45. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/design_verify/bootstrap.py +0 -0
  46. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/design_verify/browser.py +0 -0
  47. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/design_verify/compare.py +0 -0
  48. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/design_verify/loop.py +0 -0
  49. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/exceptions.py +0 -0
  50. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/git_ops.py +0 -0
  51. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/graphify.py +0 -0
  52. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/hooks/__init__.py +0 -0
  53. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/hooks/crucial_guard.py +0 -0
  54. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ipc/__init__.py +0 -0
  55. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ipc/electron_bridge.py +0 -0
  56. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ipc/handlers.py +0 -0
  57. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/local_exec.py +0 -0
  58. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/models/__init__.py +0 -0
  59. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/models/envelope.py +0 -0
  60. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/models/requests.py +0 -0
  61. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/models/responses.py +0 -0
  62. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/models/snapshots.py +0 -0
  63. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/platform_compat.py +0 -0
  64. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/py.typed +0 -0
  65. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/run_auto.py +0 -0
  66. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/sse.py +0 -0
  67. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/test.py +0 -0
  68. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/test_pipeline.py +0 -0
  69. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/__init__.py +0 -0
  70. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/dashboard.py +0 -0
  71. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/health.py +0 -0
  72. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/ip_allowlist.py +0 -0
  73. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/jobs.py +0 -0
  74. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/models.py +0 -0
  75. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/runner.py +0 -0
  76. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/security.py +0 -0
  77. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/server.py +0 -0
  78. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/service.py +0 -0
  79. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/setup_manager.py +0 -0
  80. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/static/agents.html +0 -0
  81. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/static/app.js +0 -0
  82. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/static/custom-agents.html +0 -0
  83. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/static/default-agents.html +0 -0
  84. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/static/index.html +0 -0
  85. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/static/runs.html +0 -0
  86. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/static/setup.html +0 -0
  87. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/static/styles.css +0 -0
  88. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/ui/static/tickets.html +0 -0
  89. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/sdk/updater.py +0 -0
  90. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/setup.cfg +0 -0
  91. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/tests/test_agent_chat_features.py +0 -0
  92. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/tests/test_agent_prompt_models.py +0 -0
  93. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/tests/test_agents_connectors_builder.py +0 -0
  94. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/tests/test_git_action.py +0 -0
  95. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/tests/test_health.py +0 -0
  96. {devops_bot_sdk-1.4.151 → devops_bot_sdk-1.4.153}/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.153
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.153
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
@@ -86,6 +86,7 @@ sdk/ui/static/setup.html
86
86
  sdk/ui/static/styles.css
87
87
  sdk/ui/static/tickets.html
88
88
  tests/test_agent_chat_features.py
89
+ tests/test_agent_prompt_async.py
89
90
  tests/test_agent_prompt_models.py
90
91
  tests/test_agents_connectors_builder.py
91
92
  tests/test_git_action.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.153
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.153" # 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
@@ -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:
@@ -994,55 +994,70 @@ async def _screen_agent_inputs(agent_name: str, agent_type: str, job: str) -> li
994
994
  async def agent_prompt_generate(body: AgentPromptGenerateRequest) -> dict:
995
995
  """name + type + job → a structured system prompt + {{variables}} (claude_code).
996
996
 
997
- Body: ``agent_name``, ``agent_type``, ``job``, optional ``context``.
998
- Returns ``{agent_name, agent_type, system_prompt, variables[], engine}``.
997
+ ASYNC: local Claude Code generation can take a while, so this starts a detached
998
+ job and returns ``{job_id}`` immediately (navigation-safe, and no long request
999
+ held across a proxy/gateway — avoids 504s). Poll ``GET /api/ui/jobs/{job_id}``
1000
+ for the result: ``{status, engine, ai_comments, agent_name, agent_type,
1001
+ system_prompt, variables[]}`` (or ``status="needs_revision"`` / ``"error"``).
999
1002
  """
1000
1003
  from sdk.agents.prompt_studio import build_generate_prompt, parse_generation
1004
+ from sdk.ui import jobs
1001
1005
 
1002
1006
  # Empty agent_name/agent_type/job already 422'd by the request model.
1003
1007
  agent_name = body.agent_name
1004
1008
  agent_type = body.agent_type
1005
1009
  job = body.job
1010
+ context = body.context
1011
+
1012
+ async def work():
1013
+ # Semantic screen (offensive / meaningless) → unified needs_revision. Fail-open.
1014
+ comments = await _screen_agent_inputs(agent_name, agent_type, job)
1015
+ if comments:
1016
+ return _needs_revision(comments)
1017
+ text = await _run_prompt_engine(build_generate_prompt(agent_name, agent_type, job, context))
1018
+ parsed = parse_generation(text)
1019
+ if not parsed["system_prompt"]:
1020
+ return {"status": "error", "engine": "claude_code", "ai_comments": [],
1021
+ "error": "generation returned no system_prompt"}
1022
+ return {"status": "ok", "engine": "claude_code", "ai_comments": [],
1023
+ "agent_name": agent_name, "agent_type": agent_type, **parsed}
1006
1024
 
1007
- # Semantic screen (offensive / meaningless) → unified needs_revision. Fail-open.
1008
- comments = await _screen_agent_inputs(agent_name, agent_type, job)
1009
- if comments:
1010
- return _needs_revision(comments)
1011
-
1012
- text = await _run_prompt_engine(
1013
- build_generate_prompt(agent_name, agent_type, job, body.context)
1014
- )
1015
- parsed = parse_generation(text)
1016
- if not parsed["system_prompt"]:
1017
- raise HTTPException(status_code=502, detail="generation returned no system_prompt")
1018
- return {"status": "ok", "engine": "claude_code", "ai_comments": [],
1019
- "agent_name": agent_name, "agent_type": agent_type, **parsed}
1025
+ return {"job_id": jobs.start(work())}
1020
1026
 
1021
1027
 
1022
1028
  @router.post("/agent-prompt/regenerate", dependencies=[_auth])
1023
1029
  async def agent_prompt_regenerate(body: PromptRegenerateRequest) -> dict:
1024
1030
  """Improve an existing prompt (optional feedback) → prompt + {{variables}}.
1025
1031
 
1026
- Body: ``system_prompt``, optional ``feedback``, ``agent_name``, ``agent_type``,
1027
- ``job``. Same response shape as /agent-prompt/generate.
1032
+ ASYNC (same as generate): returns ``{job_id}``; poll ``GET /api/ui/jobs/{job_id}``.
1033
+ Body: ``system_prompt``, optional ``feedback``, ``agent_name``, ``agent_type``, ``job``.
1028
1034
  """
1029
1035
  from sdk.agents.prompt_studio import build_regenerate_prompt, parse_generation
1036
+ from sdk.ui import jobs
1030
1037
 
1031
- # Empty system_prompt already 422'd by the request model. Screen the agent
1032
- # metadata (blank fields ignored by the screener; fail-open).
1033
- comments = await _screen_agent_inputs(body.agent_name, body.agent_type, body.job)
1034
- if comments:
1035
- return _needs_revision(comments)
1038
+ # Empty system_prompt already 422'd by the request model.
1039
+ agent_name = body.agent_name
1040
+ agent_type = body.agent_type
1041
+ job = body.job
1042
+ system_prompt = body.system_prompt
1043
+ feedback = body.feedback
1044
+
1045
+ async def work():
1046
+ # Screen the agent metadata (blank fields ignored by the screener; fail-open).
1047
+ comments = await _screen_agent_inputs(agent_name, agent_type, job)
1048
+ if comments:
1049
+ return _needs_revision(comments)
1050
+ text = await _run_prompt_engine(build_regenerate_prompt(
1051
+ system_prompt, feedback, agent_name, agent_type, job,
1052
+ ))
1053
+ parsed = parse_generation(text)
1054
+ if not parsed["system_prompt"]:
1055
+ return {"status": "error", "engine": "claude_code", "ai_comments": [],
1056
+ "error": "regeneration returned no system_prompt"}
1057
+ return {"status": "ok", "engine": "claude_code", "ai_comments": [],
1058
+ "agent_name": agent_name, "agent_type": agent_type, **parsed}
1036
1059
 
1037
- text = await _run_prompt_engine(build_regenerate_prompt(
1038
- body.system_prompt, body.feedback,
1039
- body.agent_name, body.agent_type, body.job,
1040
- ))
1041
- parsed = parse_generation(text)
1042
- if not parsed["system_prompt"]:
1043
- raise HTTPException(status_code=502, detail="regeneration returned no system_prompt")
1044
- return {"status": "ok", "engine": "claude_code", "ai_comments": [],
1045
- "agent_name": body.agent_name, "agent_type": body.agent_type, **parsed}
1060
+ return {"job_id": jobs.start(work())}
1046
1061
 
1047
1062
 
1048
1063
  @router.post("/agent-prompt/verify", dependencies=[_auth])
@@ -1051,53 +1066,57 @@ async def agent_prompt_verify(body: PromptVerifyRequest) -> dict:
1051
1066
 
1052
1067
  Standards check is deterministic (our Role/Objective/Rules/Workflow/Output
1053
1068
  template); secrets + variable metadata are inferred by Claude Code locally.
1054
- Body: ``system_prompt`` → ``{valid, missing_required, missing_recommended,
1055
- variables[], secrets[]}``.
1069
+ ASYNC: returns ``{job_id}``; poll ``GET /api/ui/jobs/{job_id}`` → ``{valid,
1070
+ missing_required, missing_recommended, variables[], secrets[], ...}``.
1056
1071
  """
1057
1072
  from sdk.agents.prompt_studio import build_analyze_prompt, parse_analysis
1058
1073
  from sdk.agents.prompt_template import check_system_prompt
1059
1074
  from sdk.agents.prompt_vars import extract_variables
1075
+ from sdk.ui import jobs
1060
1076
 
1061
1077
  # Too-short system_prompt already 422'd by the request model.
1062
1078
  system_prompt = body.system_prompt
1063
1079
 
1064
- standards = check_system_prompt(system_prompt)
1080
+ async def work():
1081
+ standards = check_system_prompt(system_prompt)
1065
1082
 
1066
- # Enrichment (secrets + variable metadata) via local Claude Code — best-effort:
1067
- # a failure must not sink the deterministic verdict.
1068
- analysis: dict = {"inputs": [], "secrets": []}
1069
- try:
1070
- analysis = parse_analysis(await _run_prompt_engine(build_analyze_prompt(system_prompt)))
1071
- except HTTPException:
1072
- pass
1083
+ # Enrichment (secrets + variable metadata) via local Claude Code — best-effort:
1084
+ # a failure must not sink the deterministic verdict.
1085
+ analysis: dict = {"inputs": [], "secrets": []}
1086
+ try:
1087
+ analysis = parse_analysis(await _run_prompt_engine(build_analyze_prompt(system_prompt)))
1088
+ except HTTPException:
1089
+ pass
1090
+
1091
+ meta = {str(i.get("name", "")).strip(): i for i in analysis.get("inputs", []) if isinstance(i, dict)}
1092
+ variables = []
1093
+ for name in extract_variables(system_prompt): # {{placeholders}} are authoritative
1094
+ m = meta.get(name, {})
1095
+ variables.append({
1096
+ "name": name,
1097
+ "label": str(m.get("label") or name.replace("_", " ").title()).strip(),
1098
+ "description": str(m.get("description", "")).strip(),
1099
+ "type": str(m.get("type", "string")).strip() or "string",
1100
+ })
1101
+
1102
+ # Structural gaps → the SAME unified ai_comments list (all messages together).
1103
+ ai_comments = (
1104
+ [f"Missing required section: {s}." for s in standards["missing_required"]]
1105
+ + [f"Recommended section missing: {s}." for s in standards["missing_recommended"]]
1106
+ )
1073
1107
 
1074
- meta = {str(i.get("name", "")).strip(): i for i in analysis.get("inputs", []) if isinstance(i, dict)}
1075
- variables = []
1076
- for name in extract_variables(system_prompt): # {{placeholders}} are authoritative
1077
- m = meta.get(name, {})
1078
- variables.append({
1079
- "name": name,
1080
- "label": str(m.get("label") or name.replace("_", " ").title()).strip(),
1081
- "description": str(m.get("description", "")).strip(),
1082
- "type": str(m.get("type", "string")).strip() or "string",
1083
- })
1084
-
1085
- # Structural gaps → the SAME unified ai_comments list (all messages together).
1086
- ai_comments = (
1087
- [f"Missing required section: {s}." for s in standards["missing_required"]]
1088
- + [f"Recommended section missing: {s}." for s in standards["missing_recommended"]]
1089
- )
1108
+ return {
1109
+ "status": "needs_revision" if standards["missing_required"] else "ok",
1110
+ "engine": "claude_code",
1111
+ "ai_comments": ai_comments,
1112
+ "valid": not standards["missing_required"],
1113
+ "missing_required": standards["missing_required"],
1114
+ "missing_recommended": standards["missing_recommended"],
1115
+ "variables": variables,
1116
+ "secrets": analysis.get("secrets", []),
1117
+ }
1090
1118
 
1091
- return {
1092
- "status": "needs_revision" if standards["missing_required"] else "ok",
1093
- "engine": "claude_code",
1094
- "ai_comments": ai_comments,
1095
- "valid": not standards["missing_required"],
1096
- "missing_required": standards["missing_required"],
1097
- "missing_recommended": standards["missing_recommended"],
1098
- "variables": variables,
1099
- "secrets": analysis.get("secrets", []),
1100
- }
1119
+ return {"job_id": jobs.start(work())}
1101
1120
 
1102
1121
 
1103
1122
  @router.get("/agents/form", dependencies=[_auth])
@@ -0,0 +1,74 @@
1
+ """Agent-prompt endpoints are async job-based (avoids long requests / 504s).
2
+
3
+ POST /api/ui/agent-prompt/{generate,regenerate,verify} return `{job_id}`
4
+ immediately — the local Claude Code run happens in the background; the result is
5
+ fetched from GET /api/ui/jobs/{job_id}.
6
+
7
+ Note: we assert the async *contract* (an immediate `{job_id}`). Driving the
8
+ background job to completion isn't done via the sync TestClient (each request
9
+ runs its own event loop, so a detached task from the POST doesn't advance across
10
+ later GETs) — that's a harness limitation, not a code one. The job's *result*
11
+ logic is unchanged from the pre-async version (just wrapped in `work()`).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import re
16
+ from unittest.mock import AsyncMock, patch
17
+
18
+ import pytest
19
+
20
+ fastapi = pytest.importorskip("fastapi")
21
+ from fastapi.testclient import TestClient # noqa: E402
22
+
23
+ from sdk.ui.api import require_token, router # noqa: E402
24
+
25
+ _JOB_RE = re.compile(r"^[0-9a-f]{32}$") # uuid4().hex
26
+
27
+
28
+ def _client():
29
+ app = fastapi.FastAPI()
30
+ app.include_router(router)
31
+ app.dependency_overrides[require_token] = lambda: None
32
+ return TestClient(app)
33
+
34
+
35
+ # Keep the local Claude Code call mocked so a background task (if it runs) never
36
+ # shells out to a real `claude`. Autouse across this module.
37
+ @pytest.fixture(autouse=True)
38
+ def _no_real_claude():
39
+ with patch("sdk.ui.api._run_prompt_engine", AsyncMock(return_value="<raw>")), \
40
+ patch("sdk.ui.api._screen_agent_inputs", AsyncMock(return_value=[])):
41
+ yield
42
+
43
+
44
+ class TestAsyncContract:
45
+ def test_generate_returns_job_id(self):
46
+ resp = _client().post("/api/ui/agent-prompt/generate", json={
47
+ "agent_name": "Repo Onboarder", "agent_type": "developer",
48
+ "job": "summarise a repo", "context": "", "engine": "claude_code"})
49
+ assert resp.status_code == 200
50
+ body = resp.json()
51
+ assert set(body) == {"job_id"} and _JOB_RE.match(body["job_id"])
52
+
53
+ def test_regenerate_returns_job_id(self):
54
+ resp = _client().post("/api/ui/agent-prompt/regenerate", json={
55
+ "system_prompt": "# Role\n## Objective\nDo X with {{repo_url}}.",
56
+ "feedback": "stricter", "engine": "claude_code"})
57
+ assert resp.status_code == 200
58
+ assert _JOB_RE.match(resp.json()["job_id"])
59
+
60
+ def test_verify_returns_job_id(self):
61
+ resp = _client().post("/api/ui/agent-prompt/verify", json={
62
+ "system_prompt": "# Role\n## Objective\n## Rules\n## Output\nUse {{repo_url}}."})
63
+ assert resp.status_code == 200
64
+ assert _JOB_RE.match(resp.json()["job_id"])
65
+
66
+ def test_verify_still_422_on_short_prompt(self):
67
+ # Validation stays synchronous (before the job) — no job id for bad input.
68
+ resp = _client().post("/api/ui/agent-prompt/verify", json={"system_prompt": "short"})
69
+ assert resp.status_code == 422
70
+
71
+ def test_generate_still_422_on_empty_required(self):
72
+ resp = _client().post("/api/ui/agent-prompt/generate",
73
+ json={"agent_name": "", "agent_type": "x", "job": "y"})
74
+ assert resp.status_code == 422