devops-bot-sdk 1.4.125__tar.gz → 1.4.129__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 (90) hide show
  1. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/PKG-INFO +1 -1
  2. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
  3. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
  4. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/__init__.py +2 -2
  5. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/run_auto.py +11 -0
  6. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/api.py +108 -124
  7. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/ip_allowlist.py +15 -0
  8. devops_bot_sdk-1.4.129/sdk/ui/models.py +366 -0
  9. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/server.py +13 -7
  10. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/service.py +35 -0
  11. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/README.md +0 -0
  12. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
  13. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
  14. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/devops_bot_sdk.egg-info/requires.txt +0 -0
  15. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/devops_bot_sdk.egg-info/top_level.txt +0 -0
  16. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/pyproject.toml +0 -0
  17. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agentd.py +0 -0
  18. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/__init__.py +0 -0
  19. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/builder.py +0 -0
  20. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/chat.py +0 -0
  21. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/connectors.py +0 -0
  22. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/engines/__init__.py +0 -0
  23. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/engines/claude_code.py +0 -0
  24. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/engines/http.py +0 -0
  25. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/engines/mcp.py +0 -0
  26. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/engines/n8n.py +0 -0
  27. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/engines/shell.py +0 -0
  28. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/integrations.py +0 -0
  29. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/inventory.py +0 -0
  30. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/library/__init__.py +0 -0
  31. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/manifest.py +0 -0
  32. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/prompt_studio.py +0 -0
  33. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/prompt_template.py +0 -0
  34. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/prompt_vars.py +0 -0
  35. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/run_log.py +0 -0
  36. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/secrets.py +0 -0
  37. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/usage.py +0 -0
  38. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/agents/varstore.py +0 -0
  39. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/cli.py +0 -0
  40. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/client.py +0 -0
  41. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/collectors/__init__.py +0 -0
  42. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/collectors/files.py +0 -0
  43. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/collectors/process.py +0 -0
  44. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/collectors/screenshot.py +0 -0
  45. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/config.py +0 -0
  46. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/crucial.py +0 -0
  47. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/design_verify/__init__.py +0 -0
  48. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/design_verify/assets.py +0 -0
  49. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/design_verify/bootstrap.py +0 -0
  50. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/design_verify/browser.py +0 -0
  51. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/design_verify/compare.py +0 -0
  52. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/design_verify/loop.py +0 -0
  53. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/exceptions.py +0 -0
  54. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/git_ops.py +0 -0
  55. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/graphify.py +0 -0
  56. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/hooks/__init__.py +0 -0
  57. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/hooks/crucial_guard.py +0 -0
  58. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ipc/__init__.py +0 -0
  59. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ipc/electron_bridge.py +0 -0
  60. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ipc/handlers.py +0 -0
  61. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/local_exec.py +0 -0
  62. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/models/__init__.py +0 -0
  63. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/models/envelope.py +0 -0
  64. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/models/requests.py +0 -0
  65. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/models/responses.py +0 -0
  66. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/models/snapshots.py +0 -0
  67. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/platform_compat.py +0 -0
  68. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/py.typed +0 -0
  69. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/sse.py +0 -0
  70. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/test.py +0 -0
  71. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/test_pipeline.py +0 -0
  72. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/__init__.py +0 -0
  73. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/dashboard.py +0 -0
  74. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/jobs.py +0 -0
  75. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/runner.py +0 -0
  76. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/security.py +0 -0
  77. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/setup_manager.py +0 -0
  78. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/static/agents.html +0 -0
  79. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/static/app.js +0 -0
  80. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/static/custom-agents.html +0 -0
  81. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/static/default-agents.html +0 -0
  82. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/static/index.html +0 -0
  83. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/static/runs.html +0 -0
  84. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/static/setup.html +0 -0
  85. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/static/styles.css +0 -0
  86. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/ui/static/tickets.html +0 -0
  87. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/sdk/updater.py +0 -0
  88. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/setup.cfg +0 -0
  89. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/tests/test_agent_chat_features.py +0 -0
  90. {devops_bot_sdk-1.4.125 → devops_bot_sdk-1.4.129}/tests/test_agents_connectors_builder.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devops-bot-sdk
3
- Version: 1.4.125
3
+ Version: 1.4.129
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.125
3
+ Version: 1.4.129
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
@@ -69,6 +69,7 @@ sdk/ui/api.py
69
69
  sdk/ui/dashboard.py
70
70
  sdk/ui/ip_allowlist.py
71
71
  sdk/ui/jobs.py
72
+ sdk/ui/models.py
72
73
  sdk/ui/runner.py
73
74
  sdk/ui/security.py
74
75
  sdk/ui/server.py
@@ -1,6 +1,6 @@
1
1
  """AgentOS Desktop SDK — thin HTTPS/SSE client for the Electron app.
2
2
 
3
- Version: 1.4.125
3
+ Version: 1.4.129
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.125" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
33
+ __version__ = "1.4.129" # 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
@@ -565,6 +565,17 @@ def main() -> None:
565
565
  after = current_version()
566
566
  if after != before:
567
567
  print(f"✓ updated {before} → {after}")
568
+ # Reload the control-panel server so newly-added /api/ui/* routes take
569
+ # effect — a running server keeps the code it imported at boot. Only
570
+ # restarts a managed (systemd/launchd/schtask) service; a manually-run
571
+ # panel is left alone with a hint.
572
+ try:
573
+ from sdk.ui import service as ui_service
574
+ ok, msg = ui_service.restart()
575
+ print(f" control panel: {msg}" if ok
576
+ else f" control panel: not reloaded — {msg}")
577
+ except Exception as exc: # noqa: BLE001 — never fail the update on this
578
+ print(f" control panel: not reloaded ({exc})")
568
579
  pid = _daemon_running()
569
580
  if pid:
570
581
  print("note: a background loop is running the OLD version — "
@@ -7,10 +7,29 @@ and ``sdk.config`` — no business logic is reimplemented here.
7
7
  """
8
8
  from __future__ import annotations
9
9
 
10
- from fastapi import APIRouter, Depends, HTTPException, Query, Request
10
+ from fastapi import APIRouter, Depends, HTTPException, Query, Request, Security
11
11
  from fastapi.responses import StreamingResponse
12
+ from fastapi.security import APIKeyHeader
12
13
 
13
14
  from sdk.ui import dashboard, runner, security, setup_manager
15
+ from sdk.ui.models import (
16
+ AgentChatRequest,
17
+ AgentKeysSet,
18
+ AgentPromptGenerateRequest,
19
+ AgentRunRequest,
20
+ AllowlistEntry,
21
+ ConfigureRequest,
22
+ PromptRegenerateRequest,
23
+ PromptRunRequest,
24
+ PromptVerifyRequest,
25
+ SetupRequest,
26
+ StoryRequest,
27
+ SystemChatRequest,
28
+ SystemPromptRequest,
29
+ TicketRequest,
30
+ UpdateAgentRequest,
31
+ WorkspaceRequest,
32
+ )
14
33
 
15
34
  router = APIRouter(prefix="/api/ui")
16
35
 
@@ -19,10 +38,25 @@ router = APIRouter(prefix="/api/ui")
19
38
 
20
39
  _LOOPBACK = {"127.0.0.1", "::1", "localhost"}
21
40
 
22
-
23
- async def require_token(request: Request) -> None:
41
+ # Declared PURELY so Swagger renders an "Authorize" box — paste your own co_ token
42
+ # there (nothing is ever pre-filled server-side). auto_error=False: loopback needs
43
+ # no token, and the token may also arrive via ?token= (SSE) or X-AgentOS-UI-Token,
44
+ # so the scheme documents the header without owning enforcement — that stays below.
45
+ _token_scheme = APIKeyHeader(
46
+ name="Authorization",
47
+ scheme_name="co_token",
48
+ auto_error=False,
49
+ description="Your DevOpsBot API token — paste the raw co_… value (or 'Bearer co_…').",
50
+ )
51
+
52
+
53
+ async def require_token(
54
+ request: Request,
55
+ _scheme: str | None = Security(_token_scheme),
56
+ ) -> None:
24
57
  # Same-machine (loopback) access is trusted — no token needed. Remote access
25
- # must present the configured co_ API token.
58
+ # must present the configured co_ API token. (``_scheme`` only wires the
59
+ # Swagger Authorize box; the actual token is read below to cover all forms.)
26
60
  host = (request.client.host if request.client else "") or ""
27
61
  if host in _LOOPBACK:
28
62
  return
@@ -66,15 +100,6 @@ async def status() -> dict:
66
100
  # middleware re-reads it per request so a new IP works immediately (no restart).
67
101
  # The backend proxies here (POST /api/v1/sdk/whitelist-ip) to open access for an IP.
68
102
 
69
- def _valid_ip_or_cidr(value: str) -> bool:
70
- import ipaddress
71
- try:
72
- ipaddress.ip_network(value, strict=False)
73
- return True
74
- except ValueError:
75
- return False
76
-
77
-
78
103
  @router.get("/allowlist", dependencies=[_auth])
79
104
  async def get_allowlist() -> dict:
80
105
  """This SDK's allowlist, split into permanent vs runtime.
@@ -93,20 +118,17 @@ async def get_allowlist() -> dict:
93
118
 
94
119
 
95
120
  @router.post("/allowlist", dependencies=[_auth])
96
- async def add_to_allowlist(body: dict) -> dict:
121
+ async def add_to_allowlist(body: AllowlistEntry) -> dict:
97
122
  """Whitelist an IP/CIDR for this SDK at runtime. Body: ``{ip}``. Idempotent.
98
123
 
99
124
  Opens the IP gate for that client across every path (all endpoints + UI + docs)
100
125
  until it's disabled. Effective immediately (the middleware re-reads per request).
126
+ The ``ip`` field is validated (IPv4/IPv6 address or CIDR) by ``AllowlistEntry``;
127
+ a malformed or missing value returns 422 before this handler runs.
101
128
  """
102
129
  from sdk.config import add_allowed_ip
103
130
 
104
- ip = str(body.get("ip", "")).strip()
105
- if not ip:
106
- raise HTTPException(status_code=400, detail="ip is required.")
107
- if not _valid_ip_or_cidr(ip):
108
- raise HTTPException(status_code=400, detail=f"invalid IP or CIDR: {ip!r}")
109
- return {"added": ip, "allowed_ips": add_allowed_ip(ip)}
131
+ return {"added": body.ip, "allowed_ips": add_allowed_ip(body.ip)}
110
132
 
111
133
 
112
134
  @router.delete("/allowlist", dependencies=[_auth])
@@ -121,20 +143,17 @@ async def remove_from_allowlist(ip: str = Query(...)) -> dict:
121
143
 
122
144
 
123
145
  @router.post("/configure", dependencies=[_auth])
124
- async def configure(body: dict) -> dict:
146
+ async def configure(body: ConfigureRequest) -> dict:
125
147
  from sdk import config as sdk_config
126
148
 
127
- token = str(body.get("token", "")).strip()
128
- if not token.startswith("co_"):
129
- raise HTTPException(status_code=400, detail="Token must start with 'co_'.")
130
- sdk_config.save(sdk_config.get_base_url(), token)
149
+ sdk_config.save(sdk_config.get_base_url(), body.token)
131
150
  ok, message = await dashboard.validate_token()
132
151
  return {"saved": True, "connected": ok, "message": message}
133
152
 
134
153
 
135
154
  @router.post("/setup", dependencies=[_auth])
136
- async def setup(body: dict | None = None) -> StreamingResponse:
137
- upgrade = bool((body or {}).get("upgrade", True))
155
+ async def setup(body: SetupRequest | None = None) -> StreamingResponse:
156
+ upgrade = (body or SetupRequest()).upgrade
138
157
 
139
158
  async def gen():
140
159
  async for chunk in setup_manager.reinstall_sdk(upgrade=upgrade):
@@ -168,44 +187,34 @@ async def restart_loop() -> dict:
168
187
  # ── Execution (one per agentos-auto subcommand) ────────────────────────────
169
188
 
170
189
  @router.post("/run", dependencies=[_auth])
171
- async def run(body: dict | None = None) -> dict:
190
+ async def run(body: WorkspaceRequest | None = None) -> dict:
172
191
  args = ["run"]
173
- root = (body or {}).get("workspace_root")
192
+ root = (body or WorkspaceRequest()).workspace_root
174
193
  if root:
175
- args += ["--workspace-root", str(root)]
194
+ args += ["--workspace-root", root]
176
195
  return runner.spawn_auto(*args)
177
196
 
178
197
 
179
198
  @router.post("/resume", dependencies=[_auth])
180
- async def resume(body: dict | None = None) -> dict:
199
+ async def resume(body: WorkspaceRequest | None = None) -> dict:
181
200
  args = ["resume"]
182
- root = (body or {}).get("workspace_root")
201
+ root = (body or WorkspaceRequest()).workspace_root
183
202
  if root:
184
- args += ["--workspace-root", str(root)]
203
+ args += ["--workspace-root", root]
185
204
  return runner.spawn_auto(*args)
186
205
 
187
206
 
188
207
  @router.post("/ticket", dependencies=[_auth])
189
- async def ticket(body: dict) -> dict:
190
- keys = body.get("keys") or []
191
- if isinstance(keys, str):
192
- keys = keys.split()
193
- keys = [str(k).strip() for k in keys if str(k).strip()]
194
- if not keys:
195
- raise HTTPException(status_code=400, detail="Provide at least one ticket key.")
196
- args = ["ticket", *keys]
197
- root = body.get("workspace_root")
198
- if root:
199
- args += ["--workspace-root", str(root)]
208
+ async def ticket(body: TicketRequest) -> dict:
209
+ args = ["ticket", *body.keys]
210
+ if body.workspace_root:
211
+ args += ["--workspace-root", body.workspace_root]
200
212
  return runner.spawn_auto(*args)
201
213
 
202
214
 
203
215
  @router.post("/story", dependencies=[_auth])
204
- async def story(body: dict | None = None) -> dict:
205
- keys = (body or {}).get("keys") or []
206
- if isinstance(keys, str):
207
- keys = keys.split()
208
- keys = [str(k).strip() for k in keys if str(k).strip()]
216
+ async def story(body: StoryRequest | None = None) -> dict:
217
+ keys = (body or StoryRequest()).keys
209
218
  return runner.spawn_auto("story", *keys)
210
219
 
211
220
 
@@ -410,7 +419,7 @@ async def agent_keys_list(name: str = Query(...)) -> dict:
410
419
 
411
420
 
412
421
  @router.post("/agents/keys", dependencies=[_auth])
413
- async def agent_keys_set(body: dict) -> dict:
422
+ async def agent_keys_set(body: AgentKeysSet) -> dict:
414
423
  """Persist/update a custom agent's API keys (encrypted, local, per-agent).
415
424
 
416
425
  Body: ``name``, ``keys`` ({ENV_VAR: value}). Merge semantics — keys omitted
@@ -419,14 +428,10 @@ async def agent_keys_set(body: dict) -> dict:
419
428
  """
420
429
  from sdk.agents import secrets as agent_secrets
421
430
 
422
- name = str(body.get("name", "")).strip()
423
- keys = body.get("keys") or {}
424
- if not name:
425
- raise HTTPException(status_code=400, detail="name is required.")
426
- if not isinstance(keys, dict) or not keys:
427
- raise HTTPException(status_code=400, detail="keys must be a non-empty object.")
428
- stored = agent_secrets.save_secrets(name, {str(k): str(v) for k, v in keys.items()}, merge=True)
429
- return {"name": name, "keys": sorted(stored.keys())}
431
+ stored = agent_secrets.save_secrets(
432
+ body.name, {str(k): str(v) for k, v in body.keys.items()}, merge=True
433
+ )
434
+ return {"name": body.name, "keys": sorted(stored.keys())}
430
435
 
431
436
 
432
437
  @router.delete("/agents/keys", dependencies=[_auth])
@@ -500,7 +505,7 @@ async def job_cancel(job_id: str) -> dict:
500
505
 
501
506
 
502
507
  @router.patch("/agents", dependencies=[_auth])
503
- async def update_agent(body: dict) -> dict:
508
+ async def update_agent(body: UpdateAgentRequest) -> dict:
504
509
  """Update a local custom agent (description / prompt / tools / enabled status).
505
510
 
506
511
  Re-validates and rewrites the local manifest, and best-effort syncs the change
@@ -510,20 +515,19 @@ async def update_agent(body: dict) -> dict:
510
515
 
511
516
  from sdk.agents.manifest import AGENTS_DIR, AgentManifest, load_agents
512
517
 
513
- name = str(body.get("name", "")).strip()
514
- if not name:
515
- raise HTTPException(status_code=400, detail="name is required.")
518
+ name = body.name
519
+ sent = body.model_fields_set # PATCH: only fields the caller sent are applied
516
520
  manifest = next((a for a in load_agents() if a.name == name), None)
517
521
  if manifest is None:
518
522
  raise HTTPException(status_code=404, detail=f"no local agent '{name}'")
519
523
 
520
524
  local_changes: dict = {}
521
- if "description" in body:
522
- local_changes["description"] = str(body.get("description") or "")
523
- if "prompt" in body:
524
- local_changes["prompt"] = str(body.get("prompt") or "")
525
- if "allowed_tools" in body:
526
- local_changes["allowed_tools"] = list(body.get("allowed_tools") or [])
525
+ if "description" in sent:
526
+ local_changes["description"] = str(body.description or "")
527
+ if "prompt" in sent:
528
+ local_changes["prompt"] = str(body.prompt or "")
529
+ if "allowed_tools" in sent:
530
+ local_changes["allowed_tools"] = list(body.allowed_tools or [])
527
531
 
528
532
  updated = None
529
533
  if local_changes:
@@ -536,7 +540,7 @@ async def update_agent(body: dict) -> dict:
536
540
  encoding="utf-8",
537
541
  )
538
542
 
539
- status = str(body.get("status", "")).strip()
543
+ status = (body.status or "").strip()
540
544
  # Backend sync (best-effort). Agent CREATION/authoring is not owned by the SDK
541
545
  # anymore, so a content edit is applied LOCALLY only — no re-register upsert.
542
546
  # A status change still syncs to the backend (per-agent enable/disable).
@@ -577,7 +581,7 @@ async def delete_agent(name: str = Query(...)) -> dict:
577
581
 
578
582
 
579
583
  @router.post("/agents/chat", dependencies=[_auth])
580
- async def agent_chat(body: dict) -> dict:
584
+ async def agent_chat(body: AgentChatRequest) -> dict:
581
585
  """Chat with an agent (runs its engine on this machine).
582
586
 
583
587
  Body: ``name``, ``message``, optional ``variables`` ({{var}} values), optional
@@ -587,13 +591,11 @@ async def agent_chat(body: dict) -> dict:
587
591
  """
588
592
  from sdk.ui import jobs
589
593
 
590
- name = str(body.get("name", "")).strip()
591
- message = str(body.get("message", "")).strip()
592
- if not name or not message:
593
- raise HTTPException(status_code=400, detail="name and message are required.")
594
- variables = body.get("variables") or {}
595
- history = body.get("history") or []
596
- department_id = str(body.get("department_id", "")).strip()
594
+ name = body.name
595
+ message = body.message
596
+ variables = body.variables
597
+ history = body.history
598
+ department_id = body.department_id
597
599
 
598
600
  async def work() -> dict:
599
601
  import time as _time
@@ -621,7 +623,7 @@ async def agent_chat(body: dict) -> dict:
621
623
 
622
624
 
623
625
  @router.post("/agents/run", dependencies=[_auth])
624
- async def agent_run(body: dict) -> dict:
626
+ async def agent_run(body: AgentRunRequest) -> dict:
625
627
  """Run an agent once with fresh ``{{variable}}`` values — no chat.
626
628
 
627
629
  Body: ``name``, ``variables`` ({{var}} → value; may differ every run),
@@ -633,12 +635,10 @@ async def agent_run(body: dict) -> dict:
633
635
  """
634
636
  from sdk.ui import jobs
635
637
 
636
- name = str(body.get("name", "")).strip()
637
- if not name:
638
- raise HTTPException(status_code=400, detail="name is required.")
639
- variables = body.get("variables") or {}
640
- department_id = str(body.get("department_id", "")).strip()
641
- scope = str(body.get("scope") or body.get("source") or "custom").strip() or "custom"
638
+ name = body.name
639
+ variables = body.variables
640
+ department_id = body.department_id
641
+ scope = body.scope
642
642
 
643
643
  async def work() -> dict:
644
644
  import time as _time
@@ -672,7 +672,7 @@ async def system_inventory() -> dict:
672
672
 
673
673
 
674
674
  @router.post("/system/chat", dependencies=[_auth])
675
- async def system_chat(body: dict) -> dict:
675
+ async def system_chat(body: SystemChatRequest) -> dict:
676
676
  """Ask the System Assistant about agents, subagents, tools, plugins & usage.
677
677
 
678
678
  Grounds the local model in the live inventory JSON and answers the question.
@@ -681,10 +681,8 @@ async def system_chat(body: dict) -> dict:
681
681
  """
682
682
  from sdk.ui import jobs
683
683
 
684
- message = str(body.get("message", "")).strip()
685
- if not message:
686
- raise HTTPException(status_code=400, detail="message is required.")
687
- history = body.get("history") or []
684
+ message = body.message
685
+ history = body.history
688
686
 
689
687
  async def work() -> dict:
690
688
  from pathlib import Path
@@ -732,7 +730,7 @@ async def agent_prompt(name: str = Query(...)) -> dict:
732
730
 
733
731
 
734
732
  @router.post("/prompt/analyze", dependencies=[_auth])
735
- async def prompt_analyze(body: dict) -> dict:
733
+ async def prompt_analyze(body: SystemPromptRequest) -> dict:
736
734
  """AI reads a system prompt and infers its input variables + output/format.
737
735
 
738
736
  Body: ``system_prompt``. Runs as a detached job (navigation-safe); poll
@@ -740,9 +738,7 @@ async def prompt_analyze(body: dict) -> dict:
740
738
  """
741
739
  from sdk.ui import jobs
742
740
 
743
- system_prompt = str(body.get("system_prompt", "")).strip()
744
- if len(system_prompt) < 10:
745
- raise HTTPException(status_code=400, detail="Write a system prompt first (a bit longer).")
741
+ system_prompt = body.system_prompt
746
742
 
747
743
  async def work() -> dict:
748
744
  from pathlib import Path
@@ -762,7 +758,7 @@ async def prompt_analyze(body: dict) -> dict:
762
758
 
763
759
 
764
760
  @router.post("/prompt/comments", dependencies=[_auth])
765
- async def prompt_comments(body: dict) -> dict:
761
+ async def prompt_comments(body: SystemPromptRequest) -> dict:
766
762
  """AI reviews the system prompt and returns open questions as 'comments'.
767
763
 
768
764
  Resolve these by editing the prompt BEFORE building the agent — so the agent
@@ -771,9 +767,7 @@ async def prompt_comments(body: dict) -> dict:
771
767
  """
772
768
  from sdk.ui import jobs
773
769
 
774
- system_prompt = str(body.get("system_prompt", "")).strip()
775
- if len(system_prompt) < 10:
776
- raise HTTPException(status_code=400, detail="Write a system prompt first (a bit longer).")
770
+ system_prompt = body.system_prompt
777
771
 
778
772
  async def work() -> dict:
779
773
  from pathlib import Path
@@ -793,7 +787,7 @@ async def prompt_comments(body: dict) -> dict:
793
787
 
794
788
 
795
789
  @router.post("/prompt/run", dependencies=[_auth])
796
- async def prompt_run(body: dict) -> dict:
790
+ async def prompt_run(body: PromptRunRequest) -> dict:
797
791
  """Run a system prompt with the user-supplied input values → the response.
798
792
 
799
793
  Body: ``system_prompt``, ``inputs`` ({name: value}), optional ``output_format``,
@@ -804,14 +798,12 @@ async def prompt_run(body: dict) -> dict:
804
798
  from sdk.agents.prompt_studio import sanitize_tools
805
799
  from sdk.ui import jobs
806
800
 
807
- system_prompt = str(body.get("system_prompt", "")).strip()
808
- if not system_prompt:
809
- raise HTTPException(status_code=400, detail="system_prompt is required.")
810
- inputs = body.get("inputs") or {}
811
- output_format = str(body.get("output_format", "")).strip()
801
+ system_prompt = body.system_prompt
802
+ inputs = body.inputs
803
+ output_format = body.output_format
812
804
  # AI-assessed tools; fall back to a minimal read-only tool so a pure text task
813
805
  # never inherits the broad default allowlist.
814
- tools = sanitize_tools(body.get("tools") or []) or ["Read"]
806
+ tools = sanitize_tools(body.tools) or ["Read"]
815
807
 
816
808
  async def work() -> dict:
817
809
  import time as _time
@@ -858,7 +850,7 @@ async def _run_prompt_engine(prompt: str) -> str:
858
850
 
859
851
 
860
852
  @router.post("/agent-prompt/generate", dependencies=[_auth])
861
- async def agent_prompt_generate(body: dict) -> dict:
853
+ async def agent_prompt_generate(body: AgentPromptGenerateRequest) -> dict:
862
854
  """name + type + job → a structured system prompt + {{variables}} (claude_code).
863
855
 
864
856
  Body: ``agent_name``, ``agent_type``, ``job``, optional ``context``.
@@ -866,14 +858,12 @@ async def agent_prompt_generate(body: dict) -> dict:
866
858
  """
867
859
  from sdk.agents.prompt_studio import build_generate_prompt, parse_generation
868
860
 
869
- agent_name = str(body.get("agent_name", "")).strip()
870
- agent_type = str(body.get("agent_type", "")).strip()
871
- job = str(body.get("job", "")).strip()
872
- if not agent_name or not agent_type or not job:
873
- raise HTTPException(status_code=400, detail="agent_name, agent_type, and job are required.")
861
+ agent_name = body.agent_name
862
+ agent_type = body.agent_type
863
+ job = body.job
874
864
 
875
865
  text = await _run_prompt_engine(
876
- build_generate_prompt(agent_name, agent_type, job, str(body.get("context", "")))
866
+ build_generate_prompt(agent_name, agent_type, job, body.context)
877
867
  )
878
868
  parsed = parse_generation(text)
879
869
  if not parsed["system_prompt"]:
@@ -882,7 +872,7 @@ async def agent_prompt_generate(body: dict) -> dict:
882
872
 
883
873
 
884
874
  @router.post("/agent-prompt/regenerate", dependencies=[_auth])
885
- async def agent_prompt_regenerate(body: dict) -> dict:
875
+ async def agent_prompt_regenerate(body: PromptRegenerateRequest) -> dict:
886
876
  """Improve an existing prompt (optional feedback) → prompt + {{variables}}.
887
877
 
888
878
  Body: ``system_prompt``, optional ``feedback``, ``agent_name``, ``agent_type``,
@@ -890,24 +880,20 @@ async def agent_prompt_regenerate(body: dict) -> dict:
890
880
  """
891
881
  from sdk.agents.prompt_studio import build_regenerate_prompt, parse_generation
892
882
 
893
- system_prompt = str(body.get("system_prompt", "")).strip()
894
- if not system_prompt:
895
- raise HTTPException(status_code=400, detail="system_prompt is required.")
896
-
897
883
  text = await _run_prompt_engine(build_regenerate_prompt(
898
- system_prompt, str(body.get("feedback", "")),
899
- str(body.get("agent_name", "")), str(body.get("agent_type", "")), str(body.get("job", "")),
884
+ body.system_prompt, body.feedback,
885
+ body.agent_name, body.agent_type, body.job,
900
886
  ))
901
887
  parsed = parse_generation(text)
902
888
  if not parsed["system_prompt"]:
903
889
  raise HTTPException(status_code=502, detail="regeneration returned no system_prompt")
904
- return {"agent_name": str(body.get("agent_name", "")).strip(),
905
- "agent_type": str(body.get("agent_type", "")).strip(),
890
+ return {"agent_name": body.agent_name,
891
+ "agent_type": body.agent_type,
906
892
  "engine": "claude_code", **parsed}
907
893
 
908
894
 
909
895
  @router.post("/agent-prompt/verify", dependencies=[_auth])
910
- async def agent_prompt_verify(body: dict) -> dict:
896
+ async def agent_prompt_verify(body: PromptVerifyRequest) -> dict:
911
897
  """Manual-trigger flow: verify a user-written prompt, then extract vars + secrets.
912
898
 
913
899
  Standards check is deterministic (our Role/Objective/Rules/Workflow/Output
@@ -919,9 +905,7 @@ async def agent_prompt_verify(body: dict) -> dict:
919
905
  from sdk.agents.prompt_template import check_system_prompt
920
906
  from sdk.agents.prompt_vars import extract_variables
921
907
 
922
- system_prompt = str(body.get("system_prompt", "")).strip()
923
- if len(system_prompt) < 10:
924
- raise HTTPException(status_code=400, detail="Write a system prompt first (a bit longer).")
908
+ system_prompt = body.system_prompt
925
909
 
926
910
  standards = check_system_prompt(system_prompt)
927
911
 
@@ -21,6 +21,17 @@ import os
21
21
  import socket
22
22
 
23
23
 
24
+ def public_mode() -> bool:
25
+ """Opt-in PUBLIC mode (``AGENTOS_UI_PUBLIC=1``).
26
+
27
+ When set, the SDK is a public, token-authenticated server: the IP gate is
28
+ lifted so Swagger (``/docs``) is browsable from any IP and every ``/api/ui/*``
29
+ path relies SOLELY on the ``co_`` token (``require_token``) as its credential.
30
+ Off by default — a plain launch stays locked to loopback + allowlisted IPs.
31
+ """
32
+ return os.environ.get("AGENTOS_UI_PUBLIC", "").strip().lower() in ("1", "true", "yes", "on")
33
+
34
+
24
35
  def _loopback(ip: ipaddress._BaseAddress) -> bool:
25
36
  return ip.is_loopback
26
37
 
@@ -107,6 +118,10 @@ class IPAllowlistMiddleware(BaseHTTPMiddleware):
107
118
  return any(ip in net for net in (self._local_nets + self._configured_nets()))
108
119
 
109
120
  async def dispatch(self, request: Request, call_next):
121
+ # PUBLIC mode: lift the IP gate entirely — Swagger is browsable by anyone
122
+ # and /api/ui/* is guarded only by the co_ token (require_token). Opt-in.
123
+ if public_mode():
124
+ return await call_next(request)
110
125
  # Guard EVERY path — nothing on this server is public.
111
126
  if not self._permitted(self._client_ip(request)):
112
127
  return JSONResponse(