devops-bot-sdk 1.4.117__tar.gz → 1.4.118__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.117 → devops_bot_sdk-1.4.118}/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/devops_bot_sdk.egg-info/PKG-INFO +1 -1
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/devops_bot_sdk.egg-info/SOURCES.txt +1 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/pyproject.toml +4 -1
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/__init__.py +3 -3
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/prompt_studio.py +104 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/client.py +84 -1
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/config.py +68 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/api.py +116 -0
- devops_bot_sdk-1.4.118/sdk/ui/ip_allowlist.py +110 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/security.py +9 -2
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/server.py +16 -3
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/README.md +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/devops_bot_sdk.egg-info/dependency_links.txt +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/devops_bot_sdk.egg-info/entry_points.txt +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/devops_bot_sdk.egg-info/requires.txt +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/devops_bot_sdk.egg-info/top_level.txt +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agentd.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/__init__.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/builder.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/chat.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/connectors.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/engines/__init__.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/engines/claude_code.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/engines/http.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/engines/mcp.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/engines/n8n.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/engines/shell.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/integrations.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/inventory.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/library/__init__.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/manifest.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/prompt_template.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/prompt_vars.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/run_log.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/secrets.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/usage.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/agents/varstore.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/cli.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/collectors/__init__.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/collectors/files.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/collectors/process.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/collectors/screenshot.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/crucial.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/design_verify/__init__.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/design_verify/assets.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/design_verify/bootstrap.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/design_verify/browser.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/design_verify/compare.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/design_verify/loop.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/exceptions.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/git_ops.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/graphify.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/hooks/__init__.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/hooks/crucial_guard.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ipc/__init__.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ipc/electron_bridge.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ipc/handlers.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/local_exec.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/models/__init__.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/models/envelope.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/models/requests.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/models/responses.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/models/snapshots.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/platform_compat.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/py.typed +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/run_auto.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/sse.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/test.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/test_pipeline.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/__init__.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/dashboard.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/jobs.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/runner.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/service.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/setup_manager.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/static/agents.html +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/static/app.js +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/static/custom-agents.html +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/static/default-agents.html +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/static/index.html +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/static/prompt-studio.html +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/static/runs.html +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/static/setup.html +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/static/styles.css +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/ui/static/tickets.html +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/sdk/updater.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/setup.cfg +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/tests/test_agent_chat_features.py +0 -0
- {devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/tests/test_agents_connectors_builder.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devops-bot-sdk"
|
|
7
|
-
|
|
7
|
+
dynamic = ["version"] # single source of truth: sdk/__init__.py __version__
|
|
8
8
|
description = "DevOps Bot Desktop SDK — thin client for the AgentOS Electron desktop app"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "LicenseRef-Proprietary"
|
|
@@ -49,6 +49,9 @@ agentos-ui = "sdk.ui.server:main" # local web control panel (needs the
|
|
|
49
49
|
Homepage = "https://agentos.io"
|
|
50
50
|
Repository = "https://github.com/consultancy-outfit/DevOpsBot-SDK"
|
|
51
51
|
|
|
52
|
+
[tool.setuptools.dynamic]
|
|
53
|
+
version = { attr = "sdk.__version__" } # bump sdk/__init__.py → wheel + client + server all follow
|
|
54
|
+
|
|
52
55
|
[tool.setuptools.packages.find]
|
|
53
56
|
where = ["."]
|
|
54
57
|
include = ["sdk*"]
|
|
@@ -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.118
|
|
4
4
|
|
|
5
5
|
Public surface:
|
|
6
6
|
BackendClient.from_config() — create client from ~/.agentos/config.toml
|
|
@@ -30,8 +30,8 @@ Rules:
|
|
|
30
30
|
- All data egress through submit_webhook only
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
__version__ = "1.4.
|
|
34
|
-
__author__ = "AgentOS"
|
|
33
|
+
__version__ = "1.4.118" # SINGLE SOURCE OF TRUTH — bump on every change; pyproject,
|
|
34
|
+
__author__ = "AgentOS" # sdk.client.SDK_VERSION and the UI server version all read this.
|
|
35
35
|
|
|
36
36
|
from sdk.client import BackendClient
|
|
37
37
|
from sdk.exceptions import (
|
|
@@ -92,6 +92,110 @@ def build_comments_prompt(system_prompt: str) -> str:
|
|
|
92
92
|
return COMMENTS_INSTRUCTION.replace("__SYSTEM_PROMPT__", system_prompt.strip())
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
# ── Generate: name + type + job → a full system prompt with {{variables}} ────
|
|
96
|
+
# This is the claude_code engine's authoring instruction. It runs LOCALLY in the
|
|
97
|
+
# SDK (run_claude_local); the AI backend calls this SDK endpoint on the VPS.
|
|
98
|
+
|
|
99
|
+
GENERATE_INSTRUCTION = (
|
|
100
|
+
"You are an expert prompt engineer. Write the SYSTEM PROMPT for a reusable AI "
|
|
101
|
+
"agent, given its NAME, TYPE, and main JOB. Produce a single, self-contained "
|
|
102
|
+
"prompt that follows EXACTLY this structure and Markdown headings:\n"
|
|
103
|
+
"# Role\nYou are <NAME>, <one-line identity from the type and job>.\n"
|
|
104
|
+
"## Objective\n<one paragraph: the single outcome this agent must produce>\n"
|
|
105
|
+
"## Rules\n- <hard constraints and guardrails>\n"
|
|
106
|
+
"- Never embed secrets; reference credentials as ${ENV_VAR} placeholders.\n"
|
|
107
|
+
"## Workflow\n1. <ordered steps to do the job>\n"
|
|
108
|
+
"## Output\n<exactly what the agent returns and in what format>\n\n"
|
|
109
|
+
"VARIABLE RULE (critical): any value the user must supply AT RUN TIME must "
|
|
110
|
+
"appear in the prompt text as {{variable_name}} (double curly braces, "
|
|
111
|
+
"snake_case). Only create a variable when the job genuinely needs a per-run "
|
|
112
|
+
"input; a self-contained agent may have NONE. Do not invent placeholders for "
|
|
113
|
+
"things the agent can work out itself.\n"
|
|
114
|
+
"Return ONLY a single minified JSON object, no prose, of the exact shape:\n"
|
|
115
|
+
'{"system_prompt":"<full prompt text with \\n line breaks and {{vars}} inline>",'
|
|
116
|
+
'"variables":[{"name":"","label":"","description":"","type":"string"}]}\n'
|
|
117
|
+
"Every {{variable}} in system_prompt MUST have a matching entry in variables, "
|
|
118
|
+
"and vice-versa. If there are none, return \"variables\": [].\n\n"
|
|
119
|
+
"AGENT NAME: __NAME__\nAGENT TYPE: __TYPE__\nMAIN JOB: __JOB____CONTEXT__"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
REGENERATE_INSTRUCTION = (
|
|
123
|
+
"You are an expert prompt engineer. REGENERATE and improve the EXISTING system "
|
|
124
|
+
"prompt below. The same rules as a fresh prompt apply: keep the exact structure "
|
|
125
|
+
"(# Role / ## Objective / ## Rules / ## Workflow / ## Output), keep "
|
|
126
|
+
"{{variable_name}} placeholders for every per-run input, and keep every "
|
|
127
|
+
"{{variable}} and the variables list in sync. Preserve the original intent and "
|
|
128
|
+
"any working variables; fix structure, clarity, missing sections, weak rules, "
|
|
129
|
+
"and vague output specs. Apply the FEEDBACK if given.\n"
|
|
130
|
+
"Return ONLY a single minified JSON object of the exact shape:\n"
|
|
131
|
+
'{"system_prompt":"...","variables":[{"name":"","label":"","description":"","type":"string"}]}\n\n'
|
|
132
|
+
"__META__FEEDBACK: __FEEDBACK__\n\nEXISTING SYSTEM PROMPT:\n```\n__PROMPT__\n```"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def build_generate_prompt(agent_name: str, agent_type: str, job: str,
|
|
137
|
+
context: str = "") -> str:
|
|
138
|
+
ctx = f"\nADDITIONAL CONTEXT: {context.strip()}" if context and context.strip() else ""
|
|
139
|
+
return (
|
|
140
|
+
GENERATE_INSTRUCTION
|
|
141
|
+
.replace("__NAME__", (agent_name or "").strip())
|
|
142
|
+
.replace("__TYPE__", (agent_type or "").strip())
|
|
143
|
+
.replace("__JOB__", (job or "").strip())
|
|
144
|
+
.replace("__CONTEXT__", ctx)
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def build_regenerate_prompt(system_prompt: str, feedback: str = "",
|
|
149
|
+
agent_name: str = "", agent_type: str = "",
|
|
150
|
+
job: str = "") -> str:
|
|
151
|
+
meta_bits = []
|
|
152
|
+
if (agent_name or "").strip():
|
|
153
|
+
meta_bits.append(f"AGENT NAME: {agent_name.strip()}")
|
|
154
|
+
if (agent_type or "").strip():
|
|
155
|
+
meta_bits.append(f"AGENT TYPE: {agent_type.strip()}")
|
|
156
|
+
if (job or "").strip():
|
|
157
|
+
meta_bits.append(f"MAIN JOB: {job.strip()}")
|
|
158
|
+
meta = ("\n".join(meta_bits) + "\n") if meta_bits else ""
|
|
159
|
+
return (
|
|
160
|
+
REGENERATE_INSTRUCTION
|
|
161
|
+
.replace("__META__", meta)
|
|
162
|
+
.replace("__FEEDBACK__", (feedback or "").strip() or "(none — just improve quality)")
|
|
163
|
+
.replace("__PROMPT__", (system_prompt or "").strip())
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def parse_generation(model_output: str) -> dict:
|
|
168
|
+
"""Parse a generate/regenerate model reply into ``{system_prompt, variables}``.
|
|
169
|
+
|
|
170
|
+
Placeholders present in the produced prompt are authoritative for the variable
|
|
171
|
+
LIST (so prompt ↔ schema can't drift); the model's ``variables`` array only
|
|
172
|
+
supplies each one's label/description/type. Tolerant of fences/prose.
|
|
173
|
+
"""
|
|
174
|
+
from sdk.agents.prompt_vars import extract_variables
|
|
175
|
+
|
|
176
|
+
data = _extract_json(model_output)
|
|
177
|
+
system_prompt = str(data.get("system_prompt", "")).strip() if isinstance(data, dict) else ""
|
|
178
|
+
|
|
179
|
+
meta: dict[str, dict] = {}
|
|
180
|
+
for v in (data.get("variables", []) if isinstance(data, dict) else []):
|
|
181
|
+
if isinstance(v, dict) and str(v.get("name", "")).strip():
|
|
182
|
+
meta[str(v["name"]).strip()] = v
|
|
183
|
+
elif isinstance(v, str) and v.strip():
|
|
184
|
+
meta[v.strip()] = {"name": v.strip()}
|
|
185
|
+
|
|
186
|
+
variables = []
|
|
187
|
+
for name in extract_variables(system_prompt):
|
|
188
|
+
m = meta.get(name, {})
|
|
189
|
+
vtype = str(m.get("type", "string")).strip() or "string"
|
|
190
|
+
variables.append({
|
|
191
|
+
"name": name,
|
|
192
|
+
"label": str(m.get("label") or name.replace("_", " ").title()).strip(),
|
|
193
|
+
"description": str(m.get("description", "")).strip(),
|
|
194
|
+
"type": vtype,
|
|
195
|
+
})
|
|
196
|
+
return {"system_prompt": system_prompt, "variables": variables}
|
|
197
|
+
|
|
198
|
+
|
|
95
199
|
def _extract_json(model_output: str) -> dict:
|
|
96
200
|
text = (model_output or "").strip()
|
|
97
201
|
if "```" in text:
|
|
@@ -59,7 +59,7 @@ def _notify_status(task_id: str, status: str) -> None:
|
|
|
59
59
|
pass
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
SDK_VERSION
|
|
62
|
+
from sdk import __version__ as SDK_VERSION # single source: sdk/__init__.py
|
|
63
63
|
_POLL_INTERVAL = 3.0
|
|
64
64
|
_POLL_TIMEOUT = 600.0
|
|
65
65
|
_ORCHESTRATE_TIMEOUT = 2700.0 # 45 min — covers approval wait + VPS execution time
|
|
@@ -1119,6 +1119,89 @@ class BackendClient:
|
|
|
1119
1119
|
_check_status(resp.status_code, self._base_url)
|
|
1120
1120
|
return resp.json()
|
|
1121
1121
|
|
|
1122
|
+
# ── Agent prompt generation ───────────────────────────────────────
|
|
1123
|
+
|
|
1124
|
+
async def generate_agent_prompt(
|
|
1125
|
+
self,
|
|
1126
|
+
agent_name: str,
|
|
1127
|
+
agent_type: str,
|
|
1128
|
+
job: str,
|
|
1129
|
+
context: str = "",
|
|
1130
|
+
) -> dict:
|
|
1131
|
+
"""POST /api/v1/agent-prompt/generate — draft a system prompt for an agent.
|
|
1132
|
+
|
|
1133
|
+
Given the agent's ``name``, ``type``, and main ``job`` (plain language), the
|
|
1134
|
+
backend returns a structured system prompt (Role/Objective/Rules/Workflow/
|
|
1135
|
+
Output) with ``{{variable}}`` placeholders for any runtime inputs, plus the
|
|
1136
|
+
extracted variable list. Shape::
|
|
1137
|
+
|
|
1138
|
+
{"agent_name": str, "agent_type": str, "system_prompt": str,
|
|
1139
|
+
"variables": [{"name","label","description","type"}], "model": str}
|
|
1140
|
+
|
|
1141
|
+
The ``{{variable}}`` convention matches ``sdk.agents.prompt_vars`` so the
|
|
1142
|
+
result feeds straight into the no-code builder / Prompt Studio.
|
|
1143
|
+
"""
|
|
1144
|
+
url = self._url("/api/v1/agent-prompt/generate")
|
|
1145
|
+
payload = {
|
|
1146
|
+
"agent_name": agent_name,
|
|
1147
|
+
"agent_type": agent_type,
|
|
1148
|
+
"job": job,
|
|
1149
|
+
"context": context,
|
|
1150
|
+
}
|
|
1151
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
1152
|
+
resp = await client.post(url, json=payload, headers=self._headers)
|
|
1153
|
+
if resp.status_code == 404:
|
|
1154
|
+
raise BackendFeatureMissing("Agent prompt generation", url)
|
|
1155
|
+
_check_status(resp.status_code, self._base_url)
|
|
1156
|
+
return resp.json()
|
|
1157
|
+
|
|
1158
|
+
async def regenerate_agent_prompt(
|
|
1159
|
+
self,
|
|
1160
|
+
system_prompt: str,
|
|
1161
|
+
feedback: str = "",
|
|
1162
|
+
agent_name: str = "",
|
|
1163
|
+
agent_type: str = "",
|
|
1164
|
+
job: str = "",
|
|
1165
|
+
) -> dict:
|
|
1166
|
+
"""POST /api/v1/agent-prompt/regenerate — improve an existing prompt.
|
|
1167
|
+
|
|
1168
|
+
Same response shape as :meth:`generate_agent_prompt`. ``feedback`` steers
|
|
1169
|
+
the rewrite; ``agent_name``/``agent_type``/``job`` are optional context.
|
|
1170
|
+
"""
|
|
1171
|
+
url = self._url("/api/v1/agent-prompt/regenerate")
|
|
1172
|
+
payload = {
|
|
1173
|
+
"system_prompt": system_prompt,
|
|
1174
|
+
"feedback": feedback,
|
|
1175
|
+
"agent_name": agent_name,
|
|
1176
|
+
"agent_type": agent_type,
|
|
1177
|
+
"job": job,
|
|
1178
|
+
}
|
|
1179
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
1180
|
+
resp = await client.post(url, json=payload, headers=self._headers)
|
|
1181
|
+
if resp.status_code == 404:
|
|
1182
|
+
raise BackendFeatureMissing("Agent prompt regeneration", url)
|
|
1183
|
+
_check_status(resp.status_code, self._base_url)
|
|
1184
|
+
return resp.json()
|
|
1185
|
+
|
|
1186
|
+
async def verify_agent_prompt(self, system_prompt: str) -> dict:
|
|
1187
|
+
"""POST /api/v1/agent-prompt/verify — manual-trigger verification.
|
|
1188
|
+
|
|
1189
|
+
For prompts a user wrote by hand: checks the prompt against our standards
|
|
1190
|
+
(Role/Objective/Rules/Workflow/Output) and extracts variables + secrets for
|
|
1191
|
+
the next builder step. Shape::
|
|
1192
|
+
|
|
1193
|
+
{"valid": bool, "missing_required": [...], "missing_recommended": [...],
|
|
1194
|
+
"variables": [{"name","label","description","type"}],
|
|
1195
|
+
"secrets": [{"name","description"}]}
|
|
1196
|
+
"""
|
|
1197
|
+
url = self._url("/api/v1/agent-prompt/verify")
|
|
1198
|
+
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
1199
|
+
resp = await client.post(url, json={"system_prompt": system_prompt}, headers=self._headers)
|
|
1200
|
+
if resp.status_code == 404:
|
|
1201
|
+
raise BackendFeatureMissing("Agent prompt verification", url)
|
|
1202
|
+
_check_status(resp.status_code, self._base_url)
|
|
1203
|
+
return resp.json()
|
|
1204
|
+
|
|
1122
1205
|
# ── Custom agents ─────────────────────────────────────────────────
|
|
1123
1206
|
|
|
1124
1207
|
async def register_custom_agent(self, manifest: dict) -> dict:
|
|
@@ -13,6 +13,7 @@ import base64
|
|
|
13
13
|
import hashlib
|
|
14
14
|
import os
|
|
15
15
|
import platform
|
|
16
|
+
import re
|
|
16
17
|
import sys
|
|
17
18
|
from pathlib import Path
|
|
18
19
|
from typing import Any
|
|
@@ -122,6 +123,73 @@ def get_base_url() -> str:
|
|
|
122
123
|
return load().get("base_url", AI_DEFAULT_URL)
|
|
123
124
|
|
|
124
125
|
|
|
126
|
+
def _dotenv_value(key: str) -> str:
|
|
127
|
+
"""Read ``key`` from a ``.env`` file — dependency-free (no python-dotenv).
|
|
128
|
+
|
|
129
|
+
Looks in ``~/.agentos/.env`` first (where AgentOS lives on the VPS), then a
|
|
130
|
+
``.env`` in the current working directory. Returns "" if not found. Values may
|
|
131
|
+
be quoted; surrounding quotes are stripped.
|
|
132
|
+
"""
|
|
133
|
+
for path in (_CONFIG_DIR / ".env", Path.cwd() / ".env"):
|
|
134
|
+
try:
|
|
135
|
+
if not path.exists():
|
|
136
|
+
continue
|
|
137
|
+
for line in path.read_text().splitlines():
|
|
138
|
+
line = line.strip()
|
|
139
|
+
if not line or line.startswith("#") or "=" not in line:
|
|
140
|
+
continue
|
|
141
|
+
k, _, v = line.partition("=")
|
|
142
|
+
if k.strip() == key:
|
|
143
|
+
return v.strip().strip('"').strip("'")
|
|
144
|
+
except Exception: # noqa: BLE001 — a malformed .env must never break startup
|
|
145
|
+
continue
|
|
146
|
+
return ""
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _parse_ip_list(raw: str) -> list[str]:
|
|
150
|
+
"""Split an IP allowlist string, tolerant of JSON-ish list syntax.
|
|
151
|
+
|
|
152
|
+
Accepts ``"1.2.3.4, 5.6.7.8"``, ``1.2.3.4 5.6.7.8``, or ``["1.2.3.4","5.6.7.8"]``.
|
|
153
|
+
"""
|
|
154
|
+
raw = (raw or "").strip().strip("[]")
|
|
155
|
+
out: list[str] = []
|
|
156
|
+
for token in re.split(r"[,\s]+", raw):
|
|
157
|
+
ip = token.strip().strip('"').strip("'")
|
|
158
|
+
if ip and ip not in out:
|
|
159
|
+
out.append(ip)
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_allowed_ips() -> list[str]:
|
|
164
|
+
"""Client IPs permitted to reach the ``agentos-ui`` API (``/api/ui/*``).
|
|
165
|
+
|
|
166
|
+
This is the allowlist for the *inbound* half of the two-way link: the AI
|
|
167
|
+
backend (and only it) may call back into the SDK server. Loopback is always
|
|
168
|
+
trusted separately — this list is for the AI-backend IP(s) you whitelist.
|
|
169
|
+
|
|
170
|
+
Sources, merged (all optional), each accepting a comma/space-separated or
|
|
171
|
+
bracketed list of strings:
|
|
172
|
+
- ``.env`` key ``AGENTOS_UI_ALLOWED_IPS`` (``~/.agentos/.env`` or CWD ``.env``)
|
|
173
|
+
- process env ``AGENTOS_UI_ALLOWED_IPS``
|
|
174
|
+
- config.toml key ``allowed_ips``
|
|
175
|
+
|
|
176
|
+
Returns a de-duplicated list (empty means "loopback only"). Supplying the
|
|
177
|
+
AI-backend IP here later is all that's needed to open the link — no code
|
|
178
|
+
change. CIDR/prefix entries are supported by the middleware.
|
|
179
|
+
"""
|
|
180
|
+
sources = [
|
|
181
|
+
_dotenv_value("AGENTOS_UI_ALLOWED_IPS"),
|
|
182
|
+
os.environ.get("AGENTOS_UI_ALLOWED_IPS", ""),
|
|
183
|
+
_load_raw().get("allowed_ips", ""),
|
|
184
|
+
]
|
|
185
|
+
out: list[str] = []
|
|
186
|
+
for src in sources:
|
|
187
|
+
for ip in _parse_ip_list(src):
|
|
188
|
+
if ip not in out:
|
|
189
|
+
out.append(ip)
|
|
190
|
+
return out
|
|
191
|
+
|
|
192
|
+
|
|
125
193
|
def get_branch_name_template() -> str | None:
|
|
126
194
|
"""Org-wide branch-naming convention, if configured in config.toml.
|
|
127
195
|
|
|
@@ -837,6 +837,122 @@ async def prompt_run(body: dict) -> dict:
|
|
|
837
837
|
return {"job_id": jobs.start(work())}
|
|
838
838
|
|
|
839
839
|
|
|
840
|
+
# ── Agent prompt generation (claude_code engine, runs LOCALLY on this VPS) ───
|
|
841
|
+
# The AI backend calls these over the IP-allowlisted /api/ui surface; the response
|
|
842
|
+
# is produced by the user's own authenticated `claude` CLI on this machine — no
|
|
843
|
+
# server-side LLM. Synchronous (server-to-server): the caller awaits the result.
|
|
844
|
+
|
|
845
|
+
async def _run_prompt_engine(prompt: str) -> str:
|
|
846
|
+
"""Run Claude Code locally for a pure text/JSON authoring task, return its text."""
|
|
847
|
+
from pathlib import Path
|
|
848
|
+
|
|
849
|
+
from sdk.local_exec import run_claude_local
|
|
850
|
+
|
|
851
|
+
result = await run_claude_local(
|
|
852
|
+
prompt, str(Path.home() / ".agentos"), allowed_tools=["Read"],
|
|
853
|
+
)
|
|
854
|
+
if not result.get("ok"):
|
|
855
|
+
raise HTTPException(status_code=502, detail=result.get("error") or "generation failed")
|
|
856
|
+
return result.get("result", "") or ""
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
@router.post("/agent-prompt/generate", dependencies=[_auth])
|
|
860
|
+
async def agent_prompt_generate(body: dict) -> dict:
|
|
861
|
+
"""name + type + job → a structured system prompt + {{variables}} (claude_code).
|
|
862
|
+
|
|
863
|
+
Body: ``agent_name``, ``agent_type``, ``job``, optional ``context``.
|
|
864
|
+
Returns ``{agent_name, agent_type, system_prompt, variables[], engine}``.
|
|
865
|
+
"""
|
|
866
|
+
from sdk.agents.prompt_studio import build_generate_prompt, parse_generation
|
|
867
|
+
|
|
868
|
+
agent_name = str(body.get("agent_name", "")).strip()
|
|
869
|
+
agent_type = str(body.get("agent_type", "")).strip()
|
|
870
|
+
job = str(body.get("job", "")).strip()
|
|
871
|
+
if not agent_name or not agent_type or not job:
|
|
872
|
+
raise HTTPException(status_code=400, detail="agent_name, agent_type, and job are required.")
|
|
873
|
+
|
|
874
|
+
text = await _run_prompt_engine(
|
|
875
|
+
build_generate_prompt(agent_name, agent_type, job, str(body.get("context", "")))
|
|
876
|
+
)
|
|
877
|
+
parsed = parse_generation(text)
|
|
878
|
+
if not parsed["system_prompt"]:
|
|
879
|
+
raise HTTPException(status_code=502, detail="generation returned no system_prompt")
|
|
880
|
+
return {"agent_name": agent_name, "agent_type": agent_type, "engine": "claude_code", **parsed}
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
@router.post("/agent-prompt/regenerate", dependencies=[_auth])
|
|
884
|
+
async def agent_prompt_regenerate(body: dict) -> dict:
|
|
885
|
+
"""Improve an existing prompt (optional feedback) → prompt + {{variables}}.
|
|
886
|
+
|
|
887
|
+
Body: ``system_prompt``, optional ``feedback``, ``agent_name``, ``agent_type``,
|
|
888
|
+
``job``. Same response shape as /agent-prompt/generate.
|
|
889
|
+
"""
|
|
890
|
+
from sdk.agents.prompt_studio import build_regenerate_prompt, parse_generation
|
|
891
|
+
|
|
892
|
+
system_prompt = str(body.get("system_prompt", "")).strip()
|
|
893
|
+
if not system_prompt:
|
|
894
|
+
raise HTTPException(status_code=400, detail="system_prompt is required.")
|
|
895
|
+
|
|
896
|
+
text = await _run_prompt_engine(build_regenerate_prompt(
|
|
897
|
+
system_prompt, str(body.get("feedback", "")),
|
|
898
|
+
str(body.get("agent_name", "")), str(body.get("agent_type", "")), str(body.get("job", "")),
|
|
899
|
+
))
|
|
900
|
+
parsed = parse_generation(text)
|
|
901
|
+
if not parsed["system_prompt"]:
|
|
902
|
+
raise HTTPException(status_code=502, detail="regeneration returned no system_prompt")
|
|
903
|
+
return {"agent_name": str(body.get("agent_name", "")).strip(),
|
|
904
|
+
"agent_type": str(body.get("agent_type", "")).strip(),
|
|
905
|
+
"engine": "claude_code", **parsed}
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
@router.post("/agent-prompt/verify", dependencies=[_auth])
|
|
909
|
+
async def agent_prompt_verify(body: dict) -> dict:
|
|
910
|
+
"""Manual-trigger flow: verify a user-written prompt, then extract vars + secrets.
|
|
911
|
+
|
|
912
|
+
Standards check is deterministic (our Role/Objective/Rules/Workflow/Output
|
|
913
|
+
template); secrets + variable metadata are inferred by Claude Code locally.
|
|
914
|
+
Body: ``system_prompt`` → ``{valid, missing_required, missing_recommended,
|
|
915
|
+
variables[], secrets[]}``.
|
|
916
|
+
"""
|
|
917
|
+
from sdk.agents.prompt_studio import build_analyze_prompt, parse_analysis
|
|
918
|
+
from sdk.agents.prompt_template import check_system_prompt
|
|
919
|
+
from sdk.agents.prompt_vars import extract_variables
|
|
920
|
+
|
|
921
|
+
system_prompt = str(body.get("system_prompt", "")).strip()
|
|
922
|
+
if len(system_prompt) < 10:
|
|
923
|
+
raise HTTPException(status_code=400, detail="Write a system prompt first (a bit longer).")
|
|
924
|
+
|
|
925
|
+
standards = check_system_prompt(system_prompt)
|
|
926
|
+
|
|
927
|
+
# Enrichment (secrets + variable metadata) via local Claude Code — best-effort:
|
|
928
|
+
# a failure must not sink the deterministic verdict.
|
|
929
|
+
analysis: dict = {"inputs": [], "secrets": []}
|
|
930
|
+
try:
|
|
931
|
+
analysis = parse_analysis(await _run_prompt_engine(build_analyze_prompt(system_prompt)))
|
|
932
|
+
except HTTPException:
|
|
933
|
+
pass
|
|
934
|
+
|
|
935
|
+
meta = {str(i.get("name", "")).strip(): i for i in analysis.get("inputs", []) if isinstance(i, dict)}
|
|
936
|
+
variables = []
|
|
937
|
+
for name in extract_variables(system_prompt): # {{placeholders}} are authoritative
|
|
938
|
+
m = meta.get(name, {})
|
|
939
|
+
variables.append({
|
|
940
|
+
"name": name,
|
|
941
|
+
"label": str(m.get("label") or name.replace("_", " ").title()).strip(),
|
|
942
|
+
"description": str(m.get("description", "")).strip(),
|
|
943
|
+
"type": str(m.get("type", "string")).strip() or "string",
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
return {
|
|
947
|
+
"valid": not standards["missing_required"],
|
|
948
|
+
"missing_required": standards["missing_required"],
|
|
949
|
+
"missing_recommended": standards["missing_recommended"],
|
|
950
|
+
"variables": variables,
|
|
951
|
+
"secrets": analysis.get("secrets", []),
|
|
952
|
+
"engine": "claude_code",
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
|
|
840
956
|
@router.get("/agents/form", dependencies=[_auth])
|
|
841
957
|
async def agent_form(name: str = Query(...)) -> dict:
|
|
842
958
|
"""The saved Studio form (inputs/outputs/tools) for an agent, if any."""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""IP allowlist for the ``agentos-ui`` server — the inbound half of the two-way link.
|
|
2
|
+
|
|
3
|
+
The ENTIRE server (including ``/docs``, ``/openapi.json``, ``/healthz``, the panel
|
|
4
|
+
UI, and every ``/api/ui/*`` call) is reachable ONLY from:
|
|
5
|
+
- the machine it runs on — loopback (127.0.0.0/8, ::1) AND the host's own IP
|
|
6
|
+
addresses, so hitting it via the box's public IP works too;
|
|
7
|
+
- any IP/CIDR in ``AGENTOS_UI_ALLOWED_IPS`` or the config.toml ``allowed_ips``
|
|
8
|
+
key (see ``sdk.config.get_allowed_ips``) — i.e. the AI-backend IP you whitelist.
|
|
9
|
+
|
|
10
|
+
Every other client gets ``403`` for every path — nothing is public.
|
|
11
|
+
|
|
12
|
+
Behind a reverse proxy the direct peer is the proxy, not the real client. Set
|
|
13
|
+
``AGENTOS_UI_TRUST_FORWARDED_FOR=1`` to instead honour the left-most
|
|
14
|
+
``X-Forwarded-For`` entry — enable ONLY when a trusted proxy sets that header,
|
|
15
|
+
since clients can otherwise spoof it.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import ipaddress
|
|
20
|
+
import os
|
|
21
|
+
import socket
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _loopback(ip: ipaddress._BaseAddress) -> bool:
|
|
25
|
+
return ip.is_loopback
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse(value: str):
|
|
29
|
+
"""Parse an IP or CIDR string into a network, or None if malformed."""
|
|
30
|
+
value = (value or "").strip()
|
|
31
|
+
if not value:
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
# strict=False so a bare host like "10.0.0.5" or "10.0.0.0/24" both work.
|
|
35
|
+
return ipaddress.ip_network(value, strict=False)
|
|
36
|
+
except ValueError:
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _local_ips() -> set[str]:
|
|
41
|
+
"""Best-effort set of THIS host's own IP addresses.
|
|
42
|
+
|
|
43
|
+
Lets the box reach the server through its own public/LAN IP (not just
|
|
44
|
+
127.0.0.1) — "the system where it is installed" — without whitelisting it.
|
|
45
|
+
"""
|
|
46
|
+
ips: set[str] = set()
|
|
47
|
+
try:
|
|
48
|
+
for info in socket.getaddrinfo(socket.gethostname(), None):
|
|
49
|
+
ips.add(info[4][0])
|
|
50
|
+
except Exception: # noqa: BLE001
|
|
51
|
+
pass
|
|
52
|
+
# The primary outbound IP (covers the usual public/LAN address even when the
|
|
53
|
+
# hostname doesn't resolve to it).
|
|
54
|
+
try:
|
|
55
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
56
|
+
s.connect(("8.8.8.8", 80))
|
|
57
|
+
ips.add(s.getsockname()[0])
|
|
58
|
+
s.close()
|
|
59
|
+
except Exception: # noqa: BLE001
|
|
60
|
+
pass
|
|
61
|
+
return ips
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
65
|
+
from starlette.requests import Request
|
|
66
|
+
from starlette.responses import JSONResponse
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class IPAllowlistMiddleware(BaseHTTPMiddleware):
|
|
70
|
+
"""Reject EVERY request from clients that aren't loopback, this host, or allowlisted."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, app, allowed: list[str] | None = None) -> None:
|
|
73
|
+
super().__init__(app)
|
|
74
|
+
# Resolve the allowlist once at startup. Callers can pass an explicit list
|
|
75
|
+
# (tests); otherwise we read env + config.toml.
|
|
76
|
+
if allowed is None:
|
|
77
|
+
try:
|
|
78
|
+
from sdk.config import get_allowed_ips
|
|
79
|
+
allowed = get_allowed_ips()
|
|
80
|
+
except Exception: # noqa: BLE001 — never let config break the server
|
|
81
|
+
allowed = []
|
|
82
|
+
# Configured allowlist + this host's own IPs, all as networks.
|
|
83
|
+
entries = list(allowed) + list(_local_ips())
|
|
84
|
+
self._networks = [n for n in (_parse(a) for a in entries) if n is not None]
|
|
85
|
+
self._trust_xff = os.environ.get("AGENTOS_UI_TRUST_FORWARDED_FOR", "") in ("1", "true", "TRUE", "yes")
|
|
86
|
+
|
|
87
|
+
def _client_ip(self, request: Request) -> str:
|
|
88
|
+
if self._trust_xff:
|
|
89
|
+
xff = request.headers.get("x-forwarded-for", "")
|
|
90
|
+
if xff:
|
|
91
|
+
return xff.split(",")[0].strip()
|
|
92
|
+
return (request.client.host if request.client else "") or ""
|
|
93
|
+
|
|
94
|
+
def _permitted(self, raw_ip: str) -> bool:
|
|
95
|
+
try:
|
|
96
|
+
ip = ipaddress.ip_address(raw_ip)
|
|
97
|
+
except ValueError:
|
|
98
|
+
return False
|
|
99
|
+
if _loopback(ip):
|
|
100
|
+
return True
|
|
101
|
+
return any(ip in net for net in self._networks)
|
|
102
|
+
|
|
103
|
+
async def dispatch(self, request: Request, call_next):
|
|
104
|
+
# Guard EVERY path — nothing on this server is public.
|
|
105
|
+
if not self._permitted(self._client_ip(request)):
|
|
106
|
+
return JSONResponse(
|
|
107
|
+
{"detail": "Your IP is not allowed to access this server."},
|
|
108
|
+
status_code=403,
|
|
109
|
+
)
|
|
110
|
+
return await call_next(request)
|
|
@@ -44,8 +44,15 @@ def token_matches(candidate: str | None) -> bool:
|
|
|
44
44
|
def extract_token(headers, query_params) -> str | None:
|
|
45
45
|
"""Pull the token from an Authorization/X-header or the ?token= query param."""
|
|
46
46
|
auth = headers.get("authorization") or headers.get("Authorization")
|
|
47
|
-
if auth
|
|
48
|
-
|
|
47
|
+
if auth:
|
|
48
|
+
a = auth.strip()
|
|
49
|
+
if a.lower().startswith("bearer "):
|
|
50
|
+
return a[7:].strip()
|
|
51
|
+
# Raw ``co_<uuid>_<hex>`` apiTokenHash with no ``Bearer`` prefix — the form
|
|
52
|
+
# BackendClient and the devops-bot-ai proxy send. Accept it directly so the
|
|
53
|
+
# same token works against both the AI backend and this SDK server.
|
|
54
|
+
if a.startswith("co_"):
|
|
55
|
+
return a
|
|
49
56
|
hdr = headers.get("x-agentos-ui-token") or headers.get("X-AgentOS-UI-Token")
|
|
50
57
|
if hdr:
|
|
51
58
|
return hdr.strip()
|
|
@@ -29,8 +29,17 @@ def create_app():
|
|
|
29
29
|
|
|
30
30
|
from sdk.ui import dashboard
|
|
31
31
|
from sdk.ui.api import router as api_router
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
from sdk.ui.ip_allowlist import IPAllowlistMiddleware
|
|
33
|
+
|
|
34
|
+
# Swagger at /docs (ReDoc /redoc, schema /openapi.json). NOTE: the whole server
|
|
35
|
+
# — docs included — is locked by the IP allowlist below; only this host and
|
|
36
|
+
# whitelisted IPs can reach ANY path (data calls also need the access token).
|
|
37
|
+
from sdk import __version__ as _sdk_version
|
|
38
|
+
app = FastAPI(title="AgentOS SDK", version=_sdk_version, docs_url="/docs", redoc_url="/redoc")
|
|
39
|
+
# Inbound half of the two-way link: only loopback, this host's own IPs, and
|
|
40
|
+
# whitelisted IPs (the AI backend) may reach the server AT ALL. Configure via
|
|
41
|
+
# AGENTOS_UI_ALLOWED_IPS or the config.toml `allowed_ips` key.
|
|
42
|
+
app.add_middleware(IPAllowlistMiddleware)
|
|
34
43
|
app.include_router(api_router)
|
|
35
44
|
|
|
36
45
|
@app.get("/")
|
|
@@ -133,7 +142,11 @@ def main() -> None:
|
|
|
133
142
|
pass
|
|
134
143
|
|
|
135
144
|
import uvicorn
|
|
136
|
-
|
|
145
|
+
# proxy_headers=False: the IP allowlist must decide on the REAL TCP peer, not a
|
|
146
|
+
# client-supplied X-Forwarded-For. XFF is honoured only when the operator opts
|
|
147
|
+
# in via AGENTOS_UI_TRUST_FORWARDED_FOR (handled in IPAllowlistMiddleware).
|
|
148
|
+
uvicorn.run(create_app(), host=args.host, port=args.port,
|
|
149
|
+
log_level="warning", proxy_headers=False)
|
|
137
150
|
|
|
138
151
|
|
|
139
152
|
if __name__ == "__main__":
|
|
File without changes
|
{devops_bot_sdk-1.4.117 → devops_bot_sdk-1.4.118}/devops_bot_sdk.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|