minder-cli 0.2.0__py3-none-any.whl
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.
- minder/__init__.py +12 -0
- minder/api/routers/prompts.py +177 -0
- minder/application/__init__.py +1 -0
- minder/application/admin/__init__.py +11 -0
- minder/application/admin/dto.py +453 -0
- minder/application/admin/jobs.py +327 -0
- minder/application/admin/use_cases.py +1895 -0
- minder/auth/__init__.py +12 -0
- minder/auth/context.py +26 -0
- minder/auth/middleware.py +70 -0
- minder/auth/principal.py +59 -0
- minder/auth/rate_limiter.py +89 -0
- minder/auth/rbac.py +60 -0
- minder/auth/service.py +541 -0
- minder/bootstrap/__init__.py +9 -0
- minder/bootstrap/providers.py +109 -0
- minder/bootstrap/transport.py +807 -0
- minder/cache/__init__.py +10 -0
- minder/cache/providers.py +140 -0
- minder/chunking/__init__.py +4 -0
- minder/chunking/code_splitter.py +184 -0
- minder/chunking/splitter.py +136 -0
- minder/cli.py +1542 -0
- minder/config.py +179 -0
- minder/continuity.py +363 -0
- minder/dev.py +160 -0
- minder/embedding/__init__.py +9 -0
- minder/embedding/base.py +7 -0
- minder/embedding/local.py +65 -0
- minder/embedding/openai.py +7 -0
- minder/graph/__init__.py +11 -0
- minder/graph/edges.py +13 -0
- minder/graph/executor.py +127 -0
- minder/graph/graph.py +263 -0
- minder/graph/nodes/__init__.py +27 -0
- minder/graph/nodes/evaluator.py +21 -0
- minder/graph/nodes/guard.py +64 -0
- minder/graph/nodes/llm.py +59 -0
- minder/graph/nodes/planning.py +30 -0
- minder/graph/nodes/reasoning.py +87 -0
- minder/graph/nodes/reranker.py +141 -0
- minder/graph/nodes/retriever.py +86 -0
- minder/graph/nodes/verification.py +230 -0
- minder/graph/nodes/workflow_planner.py +250 -0
- minder/graph/runtime.py +15 -0
- minder/graph/state.py +26 -0
- minder/llm/__init__.py +5 -0
- minder/llm/base.py +14 -0
- minder/llm/local.py +381 -0
- minder/llm/openai.py +89 -0
- minder/models/__init__.py +109 -0
- minder/models/base.py +10 -0
- minder/models/client.py +137 -0
- minder/models/document.py +34 -0
- minder/models/error.py +32 -0
- minder/models/graph.py +114 -0
- minder/models/history.py +32 -0
- minder/models/job.py +62 -0
- minder/models/prompt.py +41 -0
- minder/models/repository.py +62 -0
- minder/models/rule.py +68 -0
- minder/models/session.py +51 -0
- minder/models/skill.py +52 -0
- minder/models/user.py +41 -0
- minder/models/workflow.py +35 -0
- minder/observability/__init__.py +57 -0
- minder/observability/audit.py +243 -0
- minder/observability/logging.py +253 -0
- minder/observability/metrics.py +448 -0
- minder/observability/tracing.py +215 -0
- minder/presentation/__init__.py +1 -0
- minder/presentation/http/__init__.py +1 -0
- minder/presentation/http/admin/__init__.py +3 -0
- minder/presentation/http/admin/api.py +1309 -0
- minder/presentation/http/admin/context.py +94 -0
- minder/presentation/http/admin/dashboard.py +111 -0
- minder/presentation/http/admin/jobs.py +208 -0
- minder/presentation/http/admin/memories.py +185 -0
- minder/presentation/http/admin/prompts.py +219 -0
- minder/presentation/http/admin/routes.py +127 -0
- minder/presentation/http/admin/runtime.py +650 -0
- minder/presentation/http/admin/search.py +368 -0
- minder/presentation/http/admin/skills.py +230 -0
- minder/prompts/__init__.py +646 -0
- minder/prompts/formatter.py +142 -0
- minder/resources/__init__.py +318 -0
- minder/retrieval/__init__.py +5 -0
- minder/retrieval/hybrid.py +178 -0
- minder/retrieval/mmr.py +116 -0
- minder/retrieval/multi_hop.py +115 -0
- minder/runtime.py +15 -0
- minder/server.py +145 -0
- minder/store/__init__.py +64 -0
- minder/store/document.py +115 -0
- minder/store/error.py +82 -0
- minder/store/feedback.py +114 -0
- minder/store/graph.py +588 -0
- minder/store/history.py +57 -0
- minder/store/interfaces.py +512 -0
- minder/store/milvus/__init__.py +11 -0
- minder/store/milvus/client.py +26 -0
- minder/store/milvus/collections.py +15 -0
- minder/store/milvus/vector_store.py +232 -0
- minder/store/mongodb/__init__.py +11 -0
- minder/store/mongodb/client.py +49 -0
- minder/store/mongodb/indexes.py +90 -0
- minder/store/mongodb/operational_store.py +993 -0
- minder/store/relational.py +1087 -0
- minder/store/repo_state.py +58 -0
- minder/store/rule.py +93 -0
- minder/store/vector.py +79 -0
- minder/tools/__init__.py +47 -0
- minder/tools/auth.py +94 -0
- minder/tools/graph.py +839 -0
- minder/tools/ingest.py +353 -0
- minder/tools/memory.py +381 -0
- minder/tools/query.py +307 -0
- minder/tools/registry.py +269 -0
- minder/tools/repo_scanner.py +1266 -0
- minder/tools/search.py +15 -0
- minder/tools/session.py +316 -0
- minder/tools/skills.py +899 -0
- minder/tools/workflow.py +215 -0
- minder/transport/__init__.py +4 -0
- minder/transport/base.py +286 -0
- minder/transport/sse.py +252 -0
- minder/transport/stdio.py +29 -0
- minder_cli-0.2.0.dist-info/METADATA +318 -0
- minder_cli-0.2.0.dist-info/RECORD +132 -0
- minder_cli-0.2.0.dist-info/WHEEL +4 -0
- minder_cli-0.2.0.dist-info/entry_points.txt +2 -0
- minder_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,1309 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import uuid
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
from starlette.responses import JSONResponse
|
|
9
|
+
from starlette.routing import BaseRoute, Route
|
|
10
|
+
|
|
11
|
+
from minder.application.admin.dto import (
|
|
12
|
+
ClientRepositoryResolveRequest,
|
|
13
|
+
GraphSyncRequest,
|
|
14
|
+
UpsertRepositoryBranchLinkRequest,
|
|
15
|
+
)
|
|
16
|
+
from minder.auth.principal import ClientPrincipal
|
|
17
|
+
from minder.observability.metrics import (
|
|
18
|
+
get_metrics_summary,
|
|
19
|
+
record_admin_operation,
|
|
20
|
+
record_auth_event,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .context import ADMIN_COOKIE_NAME, AdminRouteContext
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _normalize_repository_remote(repo_url: str | None) -> str | None:
|
|
29
|
+
if repo_url is None:
|
|
30
|
+
return None
|
|
31
|
+
raw_url = str(repo_url).strip()
|
|
32
|
+
if not raw_url:
|
|
33
|
+
return None
|
|
34
|
+
if raw_url.startswith("git@"):
|
|
35
|
+
host_and_path = raw_url[4:]
|
|
36
|
+
host, separator, path = host_and_path.partition(":")
|
|
37
|
+
if separator and host and path:
|
|
38
|
+
normalized_path = path.strip().lstrip("/").removesuffix(".git")
|
|
39
|
+
if normalized_path:
|
|
40
|
+
return f"git@{host}:{normalized_path}.git"
|
|
41
|
+
return raw_url
|
|
42
|
+
if (
|
|
43
|
+
raw_url.startswith("ssh://")
|
|
44
|
+
or raw_url.startswith("http://")
|
|
45
|
+
or raw_url.startswith("https://")
|
|
46
|
+
):
|
|
47
|
+
parts = urlsplit(raw_url)
|
|
48
|
+
host = parts.hostname or ""
|
|
49
|
+
path = parts.path.strip().lstrip("/").removesuffix(".git")
|
|
50
|
+
user = parts.username or "git"
|
|
51
|
+
if host and path:
|
|
52
|
+
return f"{user}@{host}:{path}.git"
|
|
53
|
+
return raw_url.rstrip("/")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _principal_can_access_candidates(
|
|
57
|
+
principal: ClientPrincipal,
|
|
58
|
+
candidates: list[str],
|
|
59
|
+
) -> bool:
|
|
60
|
+
scopes = [
|
|
61
|
+
scope.strip() for scope in principal.repo_scope if scope and scope.strip()
|
|
62
|
+
]
|
|
63
|
+
if not scopes:
|
|
64
|
+
return False
|
|
65
|
+
if "*" in scopes:
|
|
66
|
+
return True
|
|
67
|
+
normalized_candidates = [
|
|
68
|
+
candidate.rstrip("/") for candidate in candidates if candidate
|
|
69
|
+
]
|
|
70
|
+
for scope in scopes:
|
|
71
|
+
normalized_scope = scope.rstrip("/")
|
|
72
|
+
for candidate in normalized_candidates:
|
|
73
|
+
if candidate == normalized_scope:
|
|
74
|
+
return True
|
|
75
|
+
if candidate.startswith(f"{normalized_scope}/"):
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _repository_scope_candidates(
|
|
81
|
+
repository: object, payload: GraphSyncRequest
|
|
82
|
+
) -> list[str]:
|
|
83
|
+
candidates: list[str] = []
|
|
84
|
+
repo_name = getattr(repository, "repo_name", None)
|
|
85
|
+
if repo_name:
|
|
86
|
+
candidates.append(str(repo_name))
|
|
87
|
+
repo_url = getattr(repository, "repo_url", None)
|
|
88
|
+
normalized_repo_url = _normalize_repository_remote(repo_url)
|
|
89
|
+
if normalized_repo_url:
|
|
90
|
+
candidates.append(normalized_repo_url)
|
|
91
|
+
payload_remote = _normalize_repository_remote(
|
|
92
|
+
payload.sync_metadata.get("repo_remote")
|
|
93
|
+
if isinstance(payload.sync_metadata, dict)
|
|
94
|
+
else None
|
|
95
|
+
)
|
|
96
|
+
if payload_remote:
|
|
97
|
+
candidates.append(payload_remote)
|
|
98
|
+
return [candidate for candidate in candidates if candidate]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _principal_can_access_repository(
|
|
102
|
+
principal: ClientPrincipal,
|
|
103
|
+
repository: object,
|
|
104
|
+
payload: GraphSyncRequest,
|
|
105
|
+
) -> bool:
|
|
106
|
+
return _principal_can_access_candidates(
|
|
107
|
+
principal,
|
|
108
|
+
_repository_scope_candidates(repository, payload),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_admin_api_routes(context: AdminRouteContext) -> list[BaseRoute]:
|
|
113
|
+
def _public_base_url(request) -> str:
|
|
114
|
+
return str(request.base_url).rstrip("/")
|
|
115
|
+
|
|
116
|
+
async def setup_api(request):
|
|
117
|
+
if await context.use_cases.has_admin_users():
|
|
118
|
+
return JSONResponse({"error": "Admin already set up"}, status_code=403)
|
|
119
|
+
payload = await request.json()
|
|
120
|
+
username = str(payload.get("username", "")).strip()
|
|
121
|
+
email = str(payload.get("email", "")).strip()
|
|
122
|
+
display_name = str(payload.get("display_name", "")).strip()
|
|
123
|
+
password = str(payload.get("password", "")).strip() or None
|
|
124
|
+
if not all([username, email, display_name]):
|
|
125
|
+
return JSONResponse({"error": "All fields are required."}, status_code=400)
|
|
126
|
+
try:
|
|
127
|
+
result = await context.use_cases.create_initial_admin(
|
|
128
|
+
username=username,
|
|
129
|
+
email=email,
|
|
130
|
+
display_name=display_name,
|
|
131
|
+
password=password,
|
|
132
|
+
)
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
135
|
+
return JSONResponse(result, status_code=201)
|
|
136
|
+
|
|
137
|
+
async def dashboard_login_api(request):
|
|
138
|
+
"""Accept either username+password or api_key authentication."""
|
|
139
|
+
payload = await request.json()
|
|
140
|
+
|
|
141
|
+
username = str(payload.get("username", "")).strip()
|
|
142
|
+
password = str(payload.get("password", "")).strip()
|
|
143
|
+
api_key = str(payload.get("api_key", "")).strip()
|
|
144
|
+
|
|
145
|
+
if not (username and password) and not api_key:
|
|
146
|
+
return JSONResponse(
|
|
147
|
+
{"error": "Provide username + password, or an admin API key."},
|
|
148
|
+
status_code=400,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
if username and password:
|
|
153
|
+
result = await context.use_cases.login_admin_by_password(
|
|
154
|
+
username, password
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
result = await context.use_cases.login_admin(api_key)
|
|
158
|
+
except PermissionError:
|
|
159
|
+
await record_auth_event(
|
|
160
|
+
"login", "denied", client_id="dashboard", store=context.store
|
|
161
|
+
)
|
|
162
|
+
return JSONResponse({"error": "Admin role required."}, status_code=403)
|
|
163
|
+
except Exception:
|
|
164
|
+
await record_auth_event(
|
|
165
|
+
"login", "failure", client_id="dashboard", store=context.store
|
|
166
|
+
)
|
|
167
|
+
return JSONResponse({"error": "Invalid credentials."}, status_code=401)
|
|
168
|
+
|
|
169
|
+
await record_auth_event(
|
|
170
|
+
"login", "success", client_id="dashboard", store=context.store
|
|
171
|
+
)
|
|
172
|
+
response = JSONResponse({"ok": True}, status_code=200)
|
|
173
|
+
response.set_cookie(
|
|
174
|
+
ADMIN_COOKIE_NAME,
|
|
175
|
+
result["jwt"],
|
|
176
|
+
httponly=True,
|
|
177
|
+
samesite="lax",
|
|
178
|
+
secure=False,
|
|
179
|
+
path="/",
|
|
180
|
+
max_age=context.config.auth.jwt_expiry_hours * 3600,
|
|
181
|
+
)
|
|
182
|
+
return response
|
|
183
|
+
|
|
184
|
+
async def dashboard_logout_api(request):
|
|
185
|
+
del request
|
|
186
|
+
await record_auth_event(
|
|
187
|
+
"logout", "success", client_id="dashboard", store=context.store
|
|
188
|
+
)
|
|
189
|
+
response = JSONResponse({"ok": True}, status_code=200)
|
|
190
|
+
response.delete_cookie(ADMIN_COOKIE_NAME, path="/")
|
|
191
|
+
return response
|
|
192
|
+
|
|
193
|
+
async def admin_session(request):
|
|
194
|
+
try:
|
|
195
|
+
user = await context.admin_user_from_request(request)
|
|
196
|
+
except PermissionError:
|
|
197
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
200
|
+
return JSONResponse({"admin": context.use_cases.serialize_admin_session(user)})
|
|
201
|
+
|
|
202
|
+
async def dashboard_bootstrap_state(request):
|
|
203
|
+
has_admin_users = await context.use_cases.has_admin_users()
|
|
204
|
+
has_admin_session = False
|
|
205
|
+
if has_admin_users:
|
|
206
|
+
try:
|
|
207
|
+
await context.admin_user_from_request(request)
|
|
208
|
+
except Exception:
|
|
209
|
+
has_admin_session = False
|
|
210
|
+
else:
|
|
211
|
+
has_admin_session = True
|
|
212
|
+
return JSONResponse(
|
|
213
|
+
{
|
|
214
|
+
"has_admin_users": has_admin_users,
|
|
215
|
+
"has_admin_session": has_admin_session,
|
|
216
|
+
}
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
async def token_exchange(request):
|
|
220
|
+
payload = await request.json()
|
|
221
|
+
try:
|
|
222
|
+
exchange = await context.use_cases.exchange_client_key(
|
|
223
|
+
client_api_key=payload["client_api_key"],
|
|
224
|
+
requested_scopes=payload.get("requested_scopes"),
|
|
225
|
+
)
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
await record_auth_event("token_exchange", "failure", store=context.store)
|
|
228
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
229
|
+
client_slug = str(exchange.get("client_slug", "unknown"))
|
|
230
|
+
await record_auth_event(
|
|
231
|
+
"token_exchange", "success", client_id=client_slug, store=context.store
|
|
232
|
+
)
|
|
233
|
+
return JSONResponse(exchange)
|
|
234
|
+
|
|
235
|
+
async def gateway_test_connection(request):
|
|
236
|
+
payload = await request.json()
|
|
237
|
+
try:
|
|
238
|
+
result = await context.use_cases.test_client_connection(
|
|
239
|
+
payload["client_api_key"],
|
|
240
|
+
public_base_url=_public_base_url(request),
|
|
241
|
+
)
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
244
|
+
return JSONResponse(result)
|
|
245
|
+
|
|
246
|
+
async def list_clients(request):
|
|
247
|
+
try:
|
|
248
|
+
await context.admin_user_from_request(request)
|
|
249
|
+
except PermissionError:
|
|
250
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
251
|
+
except Exception as exc:
|
|
252
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
253
|
+
return JSONResponse(await context.use_cases.list_clients())
|
|
254
|
+
|
|
255
|
+
async def create_client(request):
|
|
256
|
+
try:
|
|
257
|
+
user = await context.admin_user_from_request(request)
|
|
258
|
+
except PermissionError:
|
|
259
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
260
|
+
except Exception as exc:
|
|
261
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
262
|
+
payload = await request.json()
|
|
263
|
+
try:
|
|
264
|
+
created = await context.use_cases.create_client(
|
|
265
|
+
actor_user_id=user.id,
|
|
266
|
+
name=payload["name"],
|
|
267
|
+
slug=payload["slug"],
|
|
268
|
+
description=payload.get("description", ""),
|
|
269
|
+
tool_scopes=payload.get("tool_scopes"),
|
|
270
|
+
repo_scopes=payload.get("repo_scopes"),
|
|
271
|
+
)
|
|
272
|
+
except Exception as exc:
|
|
273
|
+
await record_admin_operation(
|
|
274
|
+
"create_client", "error", actor_id=str(user.id), store=context.store
|
|
275
|
+
)
|
|
276
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
277
|
+
await record_admin_operation(
|
|
278
|
+
"create_client", "success", actor_id=str(user.id), store=context.store
|
|
279
|
+
)
|
|
280
|
+
return JSONResponse(created, status_code=201)
|
|
281
|
+
|
|
282
|
+
async def admin_clients(request):
|
|
283
|
+
if request.method == "GET":
|
|
284
|
+
return await list_clients(request)
|
|
285
|
+
return await create_client(request)
|
|
286
|
+
|
|
287
|
+
async def admin_tools(request):
|
|
288
|
+
try:
|
|
289
|
+
await context.admin_user_from_request(request)
|
|
290
|
+
except PermissionError:
|
|
291
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
292
|
+
except Exception as exc:
|
|
293
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
294
|
+
return JSONResponse({"tools": context.use_cases.list_tools()})
|
|
295
|
+
|
|
296
|
+
async def client_detail(request):
|
|
297
|
+
try:
|
|
298
|
+
user = await context.admin_user_from_request(request)
|
|
299
|
+
except PermissionError:
|
|
300
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
303
|
+
|
|
304
|
+
client_id = uuid.UUID(str(request.path_params["client_id"]))
|
|
305
|
+
if request.method == "GET":
|
|
306
|
+
try:
|
|
307
|
+
return JSONResponse(
|
|
308
|
+
await context.use_cases.get_client_detail(client_id)
|
|
309
|
+
)
|
|
310
|
+
except LookupError:
|
|
311
|
+
return JSONResponse({"error": "Client not found"}, status_code=404)
|
|
312
|
+
|
|
313
|
+
payload = await request.json()
|
|
314
|
+
try:
|
|
315
|
+
updated = await context.use_cases.update_client(
|
|
316
|
+
client_id=client_id,
|
|
317
|
+
name=payload.get("name"),
|
|
318
|
+
description=payload.get("description"),
|
|
319
|
+
repo_scopes=payload.get("repo_scopes"),
|
|
320
|
+
tool_scopes=payload.get("tool_scopes"),
|
|
321
|
+
)
|
|
322
|
+
except LookupError:
|
|
323
|
+
await record_admin_operation(
|
|
324
|
+
"update_client", "error", actor_id=str(user.id), store=context.store
|
|
325
|
+
)
|
|
326
|
+
return JSONResponse({"error": "Client not found"}, status_code=404)
|
|
327
|
+
except Exception:
|
|
328
|
+
await record_admin_operation(
|
|
329
|
+
"update_client", "error", actor_id=str(user.id), store=context.store
|
|
330
|
+
)
|
|
331
|
+
raise
|
|
332
|
+
|
|
333
|
+
await record_admin_operation(
|
|
334
|
+
"update_client", "success", actor_id=str(user.id), store=context.store
|
|
335
|
+
)
|
|
336
|
+
return JSONResponse(updated)
|
|
337
|
+
|
|
338
|
+
async def client_key_rotate(request):
|
|
339
|
+
try:
|
|
340
|
+
user = await context.admin_user_from_request(request)
|
|
341
|
+
except PermissionError:
|
|
342
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
343
|
+
except Exception as exc:
|
|
344
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
345
|
+
client_id = uuid.UUID(str(request.path_params["client_id"]))
|
|
346
|
+
try:
|
|
347
|
+
result = await context.use_cases.issue_client_key(
|
|
348
|
+
client_id=client_id,
|
|
349
|
+
actor_user_id=user.id,
|
|
350
|
+
)
|
|
351
|
+
except Exception as exc:
|
|
352
|
+
await record_admin_operation(
|
|
353
|
+
"key_rotate", "error", actor_id=str(user.id), store=context.store
|
|
354
|
+
)
|
|
355
|
+
return JSONResponse({"error": str(exc)}, status_code=404)
|
|
356
|
+
await record_admin_operation(
|
|
357
|
+
"key_rotate", "success", actor_id=str(user.id), store=context.store
|
|
358
|
+
)
|
|
359
|
+
return JSONResponse(result, status_code=201)
|
|
360
|
+
|
|
361
|
+
async def client_key_revoke(request):
|
|
362
|
+
try:
|
|
363
|
+
user = await context.admin_user_from_request(request)
|
|
364
|
+
except PermissionError:
|
|
365
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
366
|
+
except Exception as exc:
|
|
367
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
368
|
+
client_id = uuid.UUID(str(request.path_params["client_id"]))
|
|
369
|
+
try:
|
|
370
|
+
result = await context.use_cases.revoke_client_keys(
|
|
371
|
+
client_id=client_id, actor_user_id=user.id
|
|
372
|
+
)
|
|
373
|
+
await record_admin_operation(
|
|
374
|
+
"key_revoke", "success", actor_id=str(user.id), store=context.store
|
|
375
|
+
)
|
|
376
|
+
return JSONResponse(result)
|
|
377
|
+
except Exception:
|
|
378
|
+
await record_admin_operation(
|
|
379
|
+
"key_revoke", "error", actor_id=str(user.id), store=context.store
|
|
380
|
+
)
|
|
381
|
+
raise
|
|
382
|
+
|
|
383
|
+
async def client_onboarding(request):
|
|
384
|
+
try:
|
|
385
|
+
await context.admin_user_from_request(request)
|
|
386
|
+
except PermissionError:
|
|
387
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
388
|
+
except Exception as exc:
|
|
389
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
390
|
+
client_id = uuid.UUID(str(request.path_params["client_id"]))
|
|
391
|
+
try:
|
|
392
|
+
result = await context.use_cases.get_onboarding(
|
|
393
|
+
client_id,
|
|
394
|
+
public_base_url=_public_base_url(request),
|
|
395
|
+
)
|
|
396
|
+
except LookupError:
|
|
397
|
+
return JSONResponse({"error": "Client not found"}, status_code=404)
|
|
398
|
+
return JSONResponse(result)
|
|
399
|
+
|
|
400
|
+
async def admin_audit(request):
|
|
401
|
+
try:
|
|
402
|
+
await context.admin_user_from_request(request)
|
|
403
|
+
except PermissionError:
|
|
404
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
405
|
+
except Exception as exc:
|
|
406
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
407
|
+
# Allow filtering by client_id as an alias for actor_id
|
|
408
|
+
actor_id = (
|
|
409
|
+
request.query_params.get("client_id")
|
|
410
|
+
or request.query_params.get("actor_id")
|
|
411
|
+
or None
|
|
412
|
+
)
|
|
413
|
+
event_type = request.query_params.get("event_type") or None
|
|
414
|
+
outcome = request.query_params.get("outcome") or None
|
|
415
|
+
try:
|
|
416
|
+
limit = int(request.query_params.get("limit", "50"))
|
|
417
|
+
offset = int(request.query_params.get("offset", "0"))
|
|
418
|
+
except ValueError:
|
|
419
|
+
limit, offset = 50, 0
|
|
420
|
+
limit = max(1, min(limit, 200)) # cap at 200
|
|
421
|
+
offset = max(0, offset)
|
|
422
|
+
return JSONResponse(
|
|
423
|
+
await context.use_cases.list_audit(
|
|
424
|
+
actor_id=actor_id,
|
|
425
|
+
event_type=event_type,
|
|
426
|
+
outcome=outcome,
|
|
427
|
+
limit=limit,
|
|
428
|
+
offset=offset,
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# ------------------------------------------------------------------
|
|
433
|
+
# User management
|
|
434
|
+
# ------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
async def admin_users(request):
|
|
437
|
+
try:
|
|
438
|
+
await context.admin_user_from_request(request)
|
|
439
|
+
except PermissionError:
|
|
440
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
441
|
+
except Exception as exc:
|
|
442
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
443
|
+
|
|
444
|
+
if request.method == "POST":
|
|
445
|
+
try:
|
|
446
|
+
payload = await request.json()
|
|
447
|
+
except Exception:
|
|
448
|
+
return JSONResponse({"error": "Invalid JSON"}, status_code=400)
|
|
449
|
+
|
|
450
|
+
username = str(payload.get("username", "")).strip()
|
|
451
|
+
email = str(payload.get("email", "")).strip()
|
|
452
|
+
display_name = str(payload.get("display_name", "")).strip()
|
|
453
|
+
role = str(payload.get("role", "admin")).strip() or "admin"
|
|
454
|
+
password = str(payload.get("password", "")).strip() or None
|
|
455
|
+
|
|
456
|
+
if not username or not email or not display_name:
|
|
457
|
+
return JSONResponse(
|
|
458
|
+
{"error": "username, email, and display_name are required"},
|
|
459
|
+
status_code=400,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
return JSONResponse(
|
|
464
|
+
await context.use_cases.create_user(
|
|
465
|
+
username=username,
|
|
466
|
+
email=email,
|
|
467
|
+
display_name=display_name,
|
|
468
|
+
role=role,
|
|
469
|
+
password=password,
|
|
470
|
+
),
|
|
471
|
+
status_code=201,
|
|
472
|
+
)
|
|
473
|
+
except Exception as exc:
|
|
474
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
475
|
+
|
|
476
|
+
active_only = request.query_params.get("active_only", "false").lower() == "true"
|
|
477
|
+
return JSONResponse(await context.use_cases.list_users(active_only=active_only))
|
|
478
|
+
|
|
479
|
+
async def user_detail(request):
|
|
480
|
+
try:
|
|
481
|
+
await context.admin_user_from_request(request)
|
|
482
|
+
except PermissionError:
|
|
483
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
484
|
+
except Exception as exc:
|
|
485
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
486
|
+
|
|
487
|
+
user_id = uuid.UUID(str(request.path_params["user_id"]))
|
|
488
|
+
|
|
489
|
+
if request.method == "GET":
|
|
490
|
+
try:
|
|
491
|
+
return JSONResponse(await context.use_cases.get_user_detail(user_id))
|
|
492
|
+
except LookupError:
|
|
493
|
+
return JSONResponse({"error": "User not found"}, status_code=404)
|
|
494
|
+
|
|
495
|
+
if request.method == "PATCH":
|
|
496
|
+
try:
|
|
497
|
+
payload = await request.json()
|
|
498
|
+
except Exception:
|
|
499
|
+
return JSONResponse({"error": "Invalid JSON"}, status_code=400)
|
|
500
|
+
try:
|
|
501
|
+
return JSONResponse(
|
|
502
|
+
await context.use_cases.update_user(
|
|
503
|
+
user_id,
|
|
504
|
+
role=payload.get("role"),
|
|
505
|
+
is_active=payload.get("is_active"),
|
|
506
|
+
display_name=payload.get("display_name"),
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
except LookupError:
|
|
510
|
+
return JSONResponse({"error": "User not found"}, status_code=404)
|
|
511
|
+
except Exception as exc:
|
|
512
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
513
|
+
|
|
514
|
+
if request.method == "DELETE":
|
|
515
|
+
try:
|
|
516
|
+
return JSONResponse(await context.use_cases.deactivate_user(user_id))
|
|
517
|
+
except LookupError:
|
|
518
|
+
return JSONResponse({"error": "User not found"}, status_code=404)
|
|
519
|
+
|
|
520
|
+
return JSONResponse({"error": "Method not allowed"}, status_code=405)
|
|
521
|
+
|
|
522
|
+
# ------------------------------------------------------------------
|
|
523
|
+
# Workflow management
|
|
524
|
+
# ------------------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
async def admin_workflows(request):
|
|
527
|
+
try:
|
|
528
|
+
await context.admin_user_from_request(request)
|
|
529
|
+
except PermissionError:
|
|
530
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
531
|
+
except Exception as exc:
|
|
532
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
533
|
+
|
|
534
|
+
if request.method == "GET":
|
|
535
|
+
return JSONResponse(await context.use_cases.list_workflows())
|
|
536
|
+
|
|
537
|
+
if request.method == "POST":
|
|
538
|
+
try:
|
|
539
|
+
payload = await request.json()
|
|
540
|
+
except Exception:
|
|
541
|
+
return JSONResponse({"error": "Invalid JSON"}, status_code=400)
|
|
542
|
+
name = str(payload.get("name", "")).strip()
|
|
543
|
+
if not name:
|
|
544
|
+
return JSONResponse({"error": "name is required"}, status_code=400)
|
|
545
|
+
try:
|
|
546
|
+
return JSONResponse(
|
|
547
|
+
await context.use_cases.create_workflow(
|
|
548
|
+
name=name,
|
|
549
|
+
description=str(payload.get("description", "")),
|
|
550
|
+
enforcement=str(payload.get("enforcement", "strict")),
|
|
551
|
+
steps=payload.get("steps"),
|
|
552
|
+
),
|
|
553
|
+
status_code=201,
|
|
554
|
+
)
|
|
555
|
+
except Exception as exc:
|
|
556
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
557
|
+
|
|
558
|
+
return JSONResponse({"error": "Method not allowed"}, status_code=405)
|
|
559
|
+
|
|
560
|
+
async def workflow_detail(request):
|
|
561
|
+
try:
|
|
562
|
+
await context.admin_user_from_request(request)
|
|
563
|
+
except PermissionError:
|
|
564
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
565
|
+
except Exception as exc:
|
|
566
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
567
|
+
|
|
568
|
+
workflow_id = uuid.UUID(str(request.path_params["workflow_id"]))
|
|
569
|
+
|
|
570
|
+
if request.method == "GET":
|
|
571
|
+
try:
|
|
572
|
+
return JSONResponse(
|
|
573
|
+
await context.use_cases.get_workflow_detail(workflow_id)
|
|
574
|
+
)
|
|
575
|
+
except LookupError:
|
|
576
|
+
return JSONResponse({"error": "Workflow not found"}, status_code=404)
|
|
577
|
+
|
|
578
|
+
if request.method == "PATCH":
|
|
579
|
+
try:
|
|
580
|
+
payload = await request.json()
|
|
581
|
+
except Exception:
|
|
582
|
+
return JSONResponse({"error": "Invalid JSON"}, status_code=400)
|
|
583
|
+
try:
|
|
584
|
+
return JSONResponse(
|
|
585
|
+
await context.use_cases.update_workflow(
|
|
586
|
+
workflow_id,
|
|
587
|
+
name=payload.get("name"),
|
|
588
|
+
description=payload.get("description"),
|
|
589
|
+
enforcement=payload.get("enforcement"),
|
|
590
|
+
steps=payload.get("steps"),
|
|
591
|
+
)
|
|
592
|
+
)
|
|
593
|
+
except LookupError:
|
|
594
|
+
return JSONResponse({"error": "Workflow not found"}, status_code=404)
|
|
595
|
+
except Exception as exc:
|
|
596
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
597
|
+
|
|
598
|
+
if request.method == "DELETE":
|
|
599
|
+
try:
|
|
600
|
+
return JSONResponse(
|
|
601
|
+
await context.use_cases.delete_workflow(workflow_id)
|
|
602
|
+
)
|
|
603
|
+
except LookupError:
|
|
604
|
+
return JSONResponse({"error": "Workflow not found"}, status_code=404)
|
|
605
|
+
|
|
606
|
+
return JSONResponse({"error": "Method not allowed"}, status_code=405)
|
|
607
|
+
|
|
608
|
+
# ------------------------------------------------------------------
|
|
609
|
+
# Observability
|
|
610
|
+
# ------------------------------------------------------------------
|
|
611
|
+
|
|
612
|
+
async def admin_metrics_summary(request):
|
|
613
|
+
try:
|
|
614
|
+
await context.admin_user_from_request(request)
|
|
615
|
+
except PermissionError:
|
|
616
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
617
|
+
except Exception as exc:
|
|
618
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
619
|
+
|
|
620
|
+
try:
|
|
621
|
+
active_sessions = await context.store.count_active_client_sessions()
|
|
622
|
+
except Exception as exc:
|
|
623
|
+
logger.warning("Failed to count active client sessions: %s", exc)
|
|
624
|
+
active_sessions = None
|
|
625
|
+
|
|
626
|
+
client_id = request.query_params.get("client_id")
|
|
627
|
+
event_type = request.query_params.get("event_type")
|
|
628
|
+
outcome = request.query_params.get("outcome")
|
|
629
|
+
|
|
630
|
+
summary = await get_metrics_summary(
|
|
631
|
+
store=context.store,
|
|
632
|
+
active_sessions=active_sessions,
|
|
633
|
+
client_id=client_id,
|
|
634
|
+
event_type=event_type,
|
|
635
|
+
outcome=outcome,
|
|
636
|
+
)
|
|
637
|
+
return JSONResponse(summary)
|
|
638
|
+
|
|
639
|
+
# ------------------------------------------------------------------
|
|
640
|
+
# Repository management
|
|
641
|
+
# ------------------------------------------------------------------
|
|
642
|
+
|
|
643
|
+
async def admin_repositories(request):
|
|
644
|
+
try:
|
|
645
|
+
await context.admin_user_from_request(request)
|
|
646
|
+
except PermissionError:
|
|
647
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
648
|
+
except Exception as exc:
|
|
649
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
650
|
+
return JSONResponse(await context.use_cases.list_repositories())
|
|
651
|
+
|
|
652
|
+
async def repository_landscape(request):
|
|
653
|
+
try:
|
|
654
|
+
await context.admin_user_from_request(request)
|
|
655
|
+
except PermissionError:
|
|
656
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
657
|
+
except Exception as exc:
|
|
658
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
659
|
+
return JSONResponse(await context.use_cases.list_repository_landscape())
|
|
660
|
+
|
|
661
|
+
async def repository_detail(request):
|
|
662
|
+
try:
|
|
663
|
+
user = await context.admin_user_from_request(request)
|
|
664
|
+
except PermissionError:
|
|
665
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
666
|
+
except Exception as exc:
|
|
667
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
668
|
+
|
|
669
|
+
repo_id = uuid.UUID(str(request.path_params["repo_id"]))
|
|
670
|
+
if request.method == "GET":
|
|
671
|
+
try:
|
|
672
|
+
return JSONResponse(
|
|
673
|
+
await context.use_cases.get_repository_detail(repo_id)
|
|
674
|
+
)
|
|
675
|
+
except LookupError:
|
|
676
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
677
|
+
|
|
678
|
+
if request.method == "PATCH":
|
|
679
|
+
payload = await request.json()
|
|
680
|
+
try:
|
|
681
|
+
result = await context.use_cases.update_repository(
|
|
682
|
+
repo_id=repo_id,
|
|
683
|
+
name=payload.get("name"),
|
|
684
|
+
remote_url=payload.get("remote_url"),
|
|
685
|
+
default_branch=payload.get("default_branch"),
|
|
686
|
+
path=payload.get("path"),
|
|
687
|
+
)
|
|
688
|
+
except LookupError:
|
|
689
|
+
await record_admin_operation(
|
|
690
|
+
"repository_update",
|
|
691
|
+
"error",
|
|
692
|
+
actor_id=str(user.id),
|
|
693
|
+
store=context.store,
|
|
694
|
+
)
|
|
695
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
696
|
+
except ValueError as exc:
|
|
697
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
698
|
+
|
|
699
|
+
await record_admin_operation(
|
|
700
|
+
"repository_update",
|
|
701
|
+
"success",
|
|
702
|
+
actor_id=str(user.id),
|
|
703
|
+
store=context.store,
|
|
704
|
+
)
|
|
705
|
+
return JSONResponse(result)
|
|
706
|
+
|
|
707
|
+
try:
|
|
708
|
+
result = await context.use_cases.delete_repository(repo_id)
|
|
709
|
+
except LookupError:
|
|
710
|
+
await record_admin_operation(
|
|
711
|
+
"repository_delete", "error", actor_id=str(user.id), store=context.store
|
|
712
|
+
)
|
|
713
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
714
|
+
|
|
715
|
+
await record_admin_operation(
|
|
716
|
+
"repository_delete", "success", actor_id=str(user.id), store=context.store
|
|
717
|
+
)
|
|
718
|
+
return JSONResponse(result)
|
|
719
|
+
|
|
720
|
+
async def repository_graph_map(request):
|
|
721
|
+
try:
|
|
722
|
+
await context.admin_user_from_request(request)
|
|
723
|
+
except PermissionError:
|
|
724
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
725
|
+
except Exception as exc:
|
|
726
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
727
|
+
|
|
728
|
+
repo_id = uuid.UUID(str(request.path_params["repo_id"]))
|
|
729
|
+
branch = (request.query_params.get("branch") or "").strip() or None
|
|
730
|
+
try:
|
|
731
|
+
return JSONResponse(
|
|
732
|
+
await context.use_cases.get_repository_graph_map(
|
|
733
|
+
repo_id=repo_id, branch=branch
|
|
734
|
+
)
|
|
735
|
+
)
|
|
736
|
+
except LookupError:
|
|
737
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
738
|
+
except RuntimeError as exc:
|
|
739
|
+
return JSONResponse({"error": str(exc)}, status_code=503)
|
|
740
|
+
|
|
741
|
+
async def repository_graph_summary(request):
|
|
742
|
+
try:
|
|
743
|
+
await context.admin_user_from_request(request)
|
|
744
|
+
except PermissionError:
|
|
745
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
746
|
+
except Exception as exc:
|
|
747
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
748
|
+
|
|
749
|
+
repo_id = uuid.UUID(str(request.path_params["repo_id"]))
|
|
750
|
+
branch = (request.query_params.get("branch") or "").strip() or None
|
|
751
|
+
try:
|
|
752
|
+
return JSONResponse(
|
|
753
|
+
await context.use_cases.get_repository_graph_summary(
|
|
754
|
+
repo_id=repo_id, branch=branch
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
except LookupError:
|
|
758
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
759
|
+
except RuntimeError as exc:
|
|
760
|
+
return JSONResponse({"error": str(exc)}, status_code=503)
|
|
761
|
+
|
|
762
|
+
async def repository_graph_search(request):
|
|
763
|
+
try:
|
|
764
|
+
await context.admin_user_from_request(request)
|
|
765
|
+
except PermissionError:
|
|
766
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
767
|
+
except Exception as exc:
|
|
768
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
769
|
+
|
|
770
|
+
repo_id = uuid.UUID(str(request.path_params["repo_id"]))
|
|
771
|
+
query = (request.query_params.get("query") or "").strip()
|
|
772
|
+
if not query:
|
|
773
|
+
return JSONResponse({"error": "Query is required"}, status_code=400)
|
|
774
|
+
branch = (request.query_params.get("branch") or "").strip() or None
|
|
775
|
+
node_types = [
|
|
776
|
+
value.strip()
|
|
777
|
+
for value in request.query_params.getlist("node_type")
|
|
778
|
+
if value.strip()
|
|
779
|
+
]
|
|
780
|
+
languages = [
|
|
781
|
+
value.strip()
|
|
782
|
+
for value in request.query_params.getlist("language")
|
|
783
|
+
if value.strip()
|
|
784
|
+
]
|
|
785
|
+
last_states = [
|
|
786
|
+
value.strip()
|
|
787
|
+
for value in request.query_params.getlist("last_state")
|
|
788
|
+
if value.strip()
|
|
789
|
+
]
|
|
790
|
+
try:
|
|
791
|
+
limit = max(1, min(int(request.query_params.get("limit", "10")), 50))
|
|
792
|
+
except ValueError:
|
|
793
|
+
return JSONResponse({"error": "Invalid limit"}, status_code=400)
|
|
794
|
+
|
|
795
|
+
try:
|
|
796
|
+
return JSONResponse(
|
|
797
|
+
await context.use_cases.search_repository_graph(
|
|
798
|
+
repo_id=repo_id,
|
|
799
|
+
query=query,
|
|
800
|
+
branch=branch,
|
|
801
|
+
node_types=node_types or None,
|
|
802
|
+
languages=languages or None,
|
|
803
|
+
last_states=last_states or None,
|
|
804
|
+
limit=limit,
|
|
805
|
+
)
|
|
806
|
+
)
|
|
807
|
+
except LookupError:
|
|
808
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
809
|
+
except RuntimeError as exc:
|
|
810
|
+
return JSONResponse({"error": str(exc)}, status_code=503)
|
|
811
|
+
|
|
812
|
+
async def repository_graph_impact(request):
|
|
813
|
+
try:
|
|
814
|
+
await context.admin_user_from_request(request)
|
|
815
|
+
except PermissionError:
|
|
816
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
817
|
+
except Exception as exc:
|
|
818
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
819
|
+
|
|
820
|
+
repo_id = uuid.UUID(str(request.path_params["repo_id"]))
|
|
821
|
+
target = (request.query_params.get("target") or "").strip()
|
|
822
|
+
if not target:
|
|
823
|
+
return JSONResponse({"error": "Target is required"}, status_code=400)
|
|
824
|
+
branch = (request.query_params.get("branch") or "").strip() or None
|
|
825
|
+
try:
|
|
826
|
+
depth = max(1, min(int(request.query_params.get("depth", "2")), 6))
|
|
827
|
+
limit = max(1, min(int(request.query_params.get("limit", "25")), 100))
|
|
828
|
+
except ValueError:
|
|
829
|
+
return JSONResponse({"error": "Invalid depth or limit"}, status_code=400)
|
|
830
|
+
|
|
831
|
+
try:
|
|
832
|
+
return JSONResponse(
|
|
833
|
+
await context.use_cases.get_repository_graph_impact(
|
|
834
|
+
repo_id=repo_id,
|
|
835
|
+
target=target,
|
|
836
|
+
branch=branch,
|
|
837
|
+
depth=depth,
|
|
838
|
+
limit=limit,
|
|
839
|
+
)
|
|
840
|
+
)
|
|
841
|
+
except LookupError:
|
|
842
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
843
|
+
except RuntimeError as exc:
|
|
844
|
+
return JSONResponse({"error": str(exc)}, status_code=503)
|
|
845
|
+
|
|
846
|
+
# ------------------------------------------------------------------
|
|
847
|
+
# Branch management
|
|
848
|
+
# ------------------------------------------------------------------
|
|
849
|
+
|
|
850
|
+
async def repository_branches(request):
|
|
851
|
+
try:
|
|
852
|
+
user = await context.admin_user_from_request(request)
|
|
853
|
+
except PermissionError:
|
|
854
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
855
|
+
except Exception as exc:
|
|
856
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
857
|
+
|
|
858
|
+
repo_id = uuid.UUID(str(request.path_params["repo_id"]))
|
|
859
|
+
|
|
860
|
+
if request.method == "GET":
|
|
861
|
+
try:
|
|
862
|
+
return JSONResponse(
|
|
863
|
+
await context.use_cases.list_repository_branches(repo_id=repo_id)
|
|
864
|
+
)
|
|
865
|
+
except LookupError:
|
|
866
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
867
|
+
|
|
868
|
+
if request.method == "POST":
|
|
869
|
+
try:
|
|
870
|
+
payload = await request.json()
|
|
871
|
+
except Exception:
|
|
872
|
+
return JSONResponse({"error": "Invalid JSON"}, status_code=400)
|
|
873
|
+
branch = str(payload.get("branch", "")).strip()
|
|
874
|
+
if not branch:
|
|
875
|
+
return JSONResponse({"error": "branch is required"}, status_code=400)
|
|
876
|
+
try:
|
|
877
|
+
result = await context.use_cases.add_repository_branch(
|
|
878
|
+
repo_id=repo_id, branch=branch
|
|
879
|
+
)
|
|
880
|
+
await record_admin_operation(
|
|
881
|
+
"repository_branch_add",
|
|
882
|
+
"success",
|
|
883
|
+
actor_id=str(user.id),
|
|
884
|
+
store=context.store,
|
|
885
|
+
)
|
|
886
|
+
return JSONResponse(result, status_code=201)
|
|
887
|
+
except LookupError:
|
|
888
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
889
|
+
except ValueError as exc:
|
|
890
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
891
|
+
|
|
892
|
+
return JSONResponse({"error": "Method not allowed"}, status_code=405)
|
|
893
|
+
|
|
894
|
+
async def repository_branch_delete(request):
|
|
895
|
+
try:
|
|
896
|
+
user = await context.admin_user_from_request(request)
|
|
897
|
+
except PermissionError:
|
|
898
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
899
|
+
except Exception as exc:
|
|
900
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
901
|
+
|
|
902
|
+
repo_id = uuid.UUID(str(request.path_params["repo_id"]))
|
|
903
|
+
branch = str(request.path_params.get("branch", "")).strip()
|
|
904
|
+
if not branch:
|
|
905
|
+
return JSONResponse({"error": "branch is required"}, status_code=400)
|
|
906
|
+
try:
|
|
907
|
+
result = await context.use_cases.remove_repository_branch(
|
|
908
|
+
repo_id=repo_id, branch=branch
|
|
909
|
+
)
|
|
910
|
+
await record_admin_operation(
|
|
911
|
+
"repository_branch_remove",
|
|
912
|
+
"success",
|
|
913
|
+
actor_id=str(user.id),
|
|
914
|
+
store=context.store,
|
|
915
|
+
)
|
|
916
|
+
return JSONResponse(result)
|
|
917
|
+
except LookupError:
|
|
918
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
919
|
+
except ValueError as exc:
|
|
920
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
921
|
+
|
|
922
|
+
async def repository_branch_links(request):
|
|
923
|
+
try:
|
|
924
|
+
user = await context.admin_user_from_request(request)
|
|
925
|
+
except PermissionError:
|
|
926
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
927
|
+
except Exception as exc:
|
|
928
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
929
|
+
|
|
930
|
+
repo_id = uuid.UUID(str(request.path_params["repo_id"]))
|
|
931
|
+
branch = (request.query_params.get("branch") or "").strip() or None
|
|
932
|
+
|
|
933
|
+
if request.method == "GET":
|
|
934
|
+
try:
|
|
935
|
+
return JSONResponse(
|
|
936
|
+
await context.use_cases.list_repository_branch_links(
|
|
937
|
+
repo_id=repo_id,
|
|
938
|
+
branch=branch,
|
|
939
|
+
)
|
|
940
|
+
)
|
|
941
|
+
except LookupError:
|
|
942
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
943
|
+
|
|
944
|
+
try:
|
|
945
|
+
payload = UpsertRepositoryBranchLinkRequest.model_validate(
|
|
946
|
+
await request.json()
|
|
947
|
+
)
|
|
948
|
+
except ValidationError as exc:
|
|
949
|
+
return JSONResponse(
|
|
950
|
+
{
|
|
951
|
+
"error": "Invalid repository branch link payload",
|
|
952
|
+
"details": exc.errors(),
|
|
953
|
+
},
|
|
954
|
+
status_code=400,
|
|
955
|
+
)
|
|
956
|
+
|
|
957
|
+
try:
|
|
958
|
+
result = await context.use_cases.upsert_repository_branch_link(
|
|
959
|
+
repo_id=repo_id,
|
|
960
|
+
source_branch=payload.source_branch,
|
|
961
|
+
target_repo_id=payload.target_repo_id,
|
|
962
|
+
target_repo_name=payload.target_repo_name,
|
|
963
|
+
target_repo_url=payload.target_repo_url,
|
|
964
|
+
target_branch=payload.target_branch,
|
|
965
|
+
relation=payload.relation,
|
|
966
|
+
direction=payload.direction,
|
|
967
|
+
confidence=payload.confidence,
|
|
968
|
+
metadata=payload.metadata,
|
|
969
|
+
)
|
|
970
|
+
await record_admin_operation(
|
|
971
|
+
"repository_branch_link_upsert",
|
|
972
|
+
"success",
|
|
973
|
+
actor_id=str(user.id),
|
|
974
|
+
store=context.store,
|
|
975
|
+
)
|
|
976
|
+
return JSONResponse(result, status_code=201)
|
|
977
|
+
except LookupError:
|
|
978
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
979
|
+
except ValueError as exc:
|
|
980
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
981
|
+
|
|
982
|
+
async def repository_branch_link_delete(request):
|
|
983
|
+
try:
|
|
984
|
+
user = await context.admin_user_from_request(request)
|
|
985
|
+
except PermissionError:
|
|
986
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
987
|
+
except Exception as exc:
|
|
988
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
989
|
+
|
|
990
|
+
repo_id = uuid.UUID(str(request.path_params["repo_id"]))
|
|
991
|
+
link_id = str(request.path_params.get("link_id", "")).strip()
|
|
992
|
+
branch = (request.query_params.get("branch") or "").strip() or None
|
|
993
|
+
if not link_id:
|
|
994
|
+
return JSONResponse({"error": "link_id is required"}, status_code=400)
|
|
995
|
+
|
|
996
|
+
try:
|
|
997
|
+
result = await context.use_cases.delete_repository_branch_link(
|
|
998
|
+
repo_id=repo_id,
|
|
999
|
+
link_id=link_id,
|
|
1000
|
+
branch=branch,
|
|
1001
|
+
)
|
|
1002
|
+
await record_admin_operation(
|
|
1003
|
+
"repository_branch_link_delete",
|
|
1004
|
+
"success",
|
|
1005
|
+
actor_id=str(user.id),
|
|
1006
|
+
store=context.store,
|
|
1007
|
+
)
|
|
1008
|
+
return JSONResponse(result)
|
|
1009
|
+
except LookupError as exc:
|
|
1010
|
+
status_code = (
|
|
1011
|
+
404 if "Repository" in str(exc) or "not found" in str(exc) else 400
|
|
1012
|
+
)
|
|
1013
|
+
return JSONResponse({"error": str(exc)}, status_code=status_code)
|
|
1014
|
+
except ValueError as exc:
|
|
1015
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
1016
|
+
|
|
1017
|
+
async def repository_graph_sync(request):
|
|
1018
|
+
try:
|
|
1019
|
+
user = await context.admin_user_from_request(request)
|
|
1020
|
+
except PermissionError:
|
|
1021
|
+
return JSONResponse({"error": "Admin role required"}, status_code=403)
|
|
1022
|
+
except Exception as exc:
|
|
1023
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
1024
|
+
|
|
1025
|
+
if context.graph_store is None:
|
|
1026
|
+
return JSONResponse(
|
|
1027
|
+
{"error": "Graph sync store is not configured"},
|
|
1028
|
+
status_code=503,
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
repo_id = uuid.UUID(str(request.path_params["repo_id"]))
|
|
1032
|
+
try:
|
|
1033
|
+
payload = GraphSyncRequest.model_validate(await request.json())
|
|
1034
|
+
except ValidationError as exc:
|
|
1035
|
+
return JSONResponse(
|
|
1036
|
+
{"error": "Invalid graph sync payload", "details": exc.errors()},
|
|
1037
|
+
status_code=400,
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
try:
|
|
1041
|
+
result = await context.use_cases.sync_repository_graph(
|
|
1042
|
+
repo_id=repo_id,
|
|
1043
|
+
payload=payload,
|
|
1044
|
+
)
|
|
1045
|
+
except LookupError:
|
|
1046
|
+
await record_admin_operation(
|
|
1047
|
+
"repository_graph_sync",
|
|
1048
|
+
"error",
|
|
1049
|
+
actor_id=str(user.id),
|
|
1050
|
+
store=context.store,
|
|
1051
|
+
)
|
|
1052
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
1053
|
+
except RuntimeError as exc:
|
|
1054
|
+
return JSONResponse({"error": str(exc)}, status_code=503)
|
|
1055
|
+
except Exception as exc:
|
|
1056
|
+
await record_admin_operation(
|
|
1057
|
+
"repository_graph_sync",
|
|
1058
|
+
"error",
|
|
1059
|
+
actor_id=str(user.id),
|
|
1060
|
+
store=context.store,
|
|
1061
|
+
)
|
|
1062
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
1063
|
+
|
|
1064
|
+
await record_admin_operation(
|
|
1065
|
+
"repository_graph_sync",
|
|
1066
|
+
"success",
|
|
1067
|
+
actor_id=str(user.id),
|
|
1068
|
+
store=context.store,
|
|
1069
|
+
)
|
|
1070
|
+
return JSONResponse(result, status_code=202)
|
|
1071
|
+
|
|
1072
|
+
async def client_repository_graph_sync(request):
|
|
1073
|
+
try:
|
|
1074
|
+
principal = await context.client_principal_from_request(request)
|
|
1075
|
+
except PermissionError:
|
|
1076
|
+
return JSONResponse({"error": "Client principal required"}, status_code=403)
|
|
1077
|
+
except Exception as exc:
|
|
1078
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
1079
|
+
|
|
1080
|
+
if context.graph_store is None:
|
|
1081
|
+
return JSONResponse(
|
|
1082
|
+
{"error": "Graph sync store is not configured"},
|
|
1083
|
+
status_code=503,
|
|
1084
|
+
)
|
|
1085
|
+
|
|
1086
|
+
repo_id = uuid.UUID(str(request.path_params["repo_id"]))
|
|
1087
|
+
repository = await context.store.get_repository_by_id(repo_id)
|
|
1088
|
+
if repository is None:
|
|
1089
|
+
return JSONResponse({"error": "Repository not found"}, status_code=404)
|
|
1090
|
+
|
|
1091
|
+
try:
|
|
1092
|
+
payload = GraphSyncRequest.model_validate(await request.json())
|
|
1093
|
+
except ValidationError as exc:
|
|
1094
|
+
return JSONResponse(
|
|
1095
|
+
{"error": "Invalid graph sync payload", "details": exc.errors()},
|
|
1096
|
+
status_code=400,
|
|
1097
|
+
)
|
|
1098
|
+
|
|
1099
|
+
if not _principal_can_access_repository(principal, repository, payload):
|
|
1100
|
+
return JSONResponse(
|
|
1101
|
+
{"error": "Client is not allowed to sync this repository"},
|
|
1102
|
+
status_code=403,
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
try:
|
|
1106
|
+
result = await context.use_cases.sync_repository_graph(
|
|
1107
|
+
repo_id=repo_id,
|
|
1108
|
+
payload=payload,
|
|
1109
|
+
)
|
|
1110
|
+
except Exception as exc:
|
|
1111
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
1112
|
+
|
|
1113
|
+
try:
|
|
1114
|
+
await context.store.create_audit_log(
|
|
1115
|
+
actor_type="client",
|
|
1116
|
+
actor_id=str(principal.client_id),
|
|
1117
|
+
event_type="repository.graph_sync",
|
|
1118
|
+
resource_type="repository",
|
|
1119
|
+
resource_id=str(repo_id),
|
|
1120
|
+
outcome="success",
|
|
1121
|
+
audit_metadata={
|
|
1122
|
+
"client_slug": principal.client_slug,
|
|
1123
|
+
"payload_version": payload.payload_version,
|
|
1124
|
+
"source": payload.source,
|
|
1125
|
+
"nodes_upserted": result["nodes_upserted"],
|
|
1126
|
+
"edges_upserted": result["edges_upserted"],
|
|
1127
|
+
},
|
|
1128
|
+
)
|
|
1129
|
+
except Exception:
|
|
1130
|
+
logger.exception("Failed to record client graph sync audit log")
|
|
1131
|
+
|
|
1132
|
+
return JSONResponse(result, status_code=202)
|
|
1133
|
+
|
|
1134
|
+
async def client_repository_resolve(request):
|
|
1135
|
+
try:
|
|
1136
|
+
principal = await context.client_principal_from_request(request)
|
|
1137
|
+
except PermissionError:
|
|
1138
|
+
return JSONResponse({"error": "Client principal required"}, status_code=403)
|
|
1139
|
+
except Exception as exc:
|
|
1140
|
+
return JSONResponse({"error": str(exc)}, status_code=401)
|
|
1141
|
+
|
|
1142
|
+
try:
|
|
1143
|
+
payload = ClientRepositoryResolveRequest.model_validate(
|
|
1144
|
+
await request.json()
|
|
1145
|
+
)
|
|
1146
|
+
except ValidationError as exc:
|
|
1147
|
+
return JSONResponse(
|
|
1148
|
+
{
|
|
1149
|
+
"error": "Invalid repository resolve payload",
|
|
1150
|
+
"details": exc.errors(),
|
|
1151
|
+
},
|
|
1152
|
+
status_code=400,
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
candidates = [payload.repo_name, payload.repo_path]
|
|
1156
|
+
normalized_remote = _normalize_repository_remote(payload.repo_url)
|
|
1157
|
+
if normalized_remote:
|
|
1158
|
+
candidates.append(normalized_remote)
|
|
1159
|
+
|
|
1160
|
+
if not _principal_can_access_candidates(principal, candidates):
|
|
1161
|
+
return JSONResponse(
|
|
1162
|
+
{"error": "Client is not allowed to resolve this repository"},
|
|
1163
|
+
status_code=403,
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
try:
|
|
1167
|
+
result = await context.use_cases.resolve_repository_for_client(
|
|
1168
|
+
repo_name=payload.repo_name,
|
|
1169
|
+
repo_path=payload.repo_path,
|
|
1170
|
+
repo_url=payload.repo_url,
|
|
1171
|
+
default_branch=payload.default_branch,
|
|
1172
|
+
)
|
|
1173
|
+
except ValueError as exc:
|
|
1174
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
1175
|
+
except Exception as exc:
|
|
1176
|
+
return JSONResponse({"error": str(exc)}, status_code=400)
|
|
1177
|
+
|
|
1178
|
+
repository_id = result["repository"]["id"]
|
|
1179
|
+
try:
|
|
1180
|
+
await context.store.create_audit_log(
|
|
1181
|
+
actor_type="client",
|
|
1182
|
+
actor_id=str(principal.client_id),
|
|
1183
|
+
event_type="repository.resolve",
|
|
1184
|
+
resource_type="repository",
|
|
1185
|
+
resource_id=repository_id,
|
|
1186
|
+
outcome="success",
|
|
1187
|
+
audit_metadata={
|
|
1188
|
+
"client_slug": principal.client_slug,
|
|
1189
|
+
"created": result["created"],
|
|
1190
|
+
"path": result["repository"]["path"],
|
|
1191
|
+
"name": result["repository"]["name"],
|
|
1192
|
+
},
|
|
1193
|
+
)
|
|
1194
|
+
except Exception:
|
|
1195
|
+
logger.exception("Failed to record client repository resolve audit log")
|
|
1196
|
+
|
|
1197
|
+
return JSONResponse(result, status_code=201 if result["created"] else 200)
|
|
1198
|
+
|
|
1199
|
+
return [
|
|
1200
|
+
Route("/v1/admin/setup", setup_api, methods=["POST"]),
|
|
1201
|
+
Route("/v1/admin/login", dashboard_login_api, methods=["POST"]),
|
|
1202
|
+
Route("/v1/admin/logout", dashboard_logout_api, methods=["POST"]),
|
|
1203
|
+
Route("/v1/admin/session", admin_session, methods=["GET"]),
|
|
1204
|
+
Route("/v1/admin/bootstrap-state", dashboard_bootstrap_state, methods=["GET"]),
|
|
1205
|
+
Route("/v1/auth/token-exchange", token_exchange, methods=["POST"]),
|
|
1206
|
+
Route("/v1/gateway/test-connection", gateway_test_connection, methods=["POST"]),
|
|
1207
|
+
Route("/v1/admin/tools", admin_tools, methods=["GET"]),
|
|
1208
|
+
Route("/v1/admin/clients", admin_clients, methods=["GET", "POST"]),
|
|
1209
|
+
Route(
|
|
1210
|
+
"/v1/admin/clients/{client_id:uuid}",
|
|
1211
|
+
client_detail,
|
|
1212
|
+
methods=["GET", "PATCH"],
|
|
1213
|
+
),
|
|
1214
|
+
Route(
|
|
1215
|
+
"/v1/admin/clients/{client_id:uuid}/keys",
|
|
1216
|
+
client_key_rotate,
|
|
1217
|
+
methods=["POST"],
|
|
1218
|
+
),
|
|
1219
|
+
Route(
|
|
1220
|
+
"/v1/admin/clients/{client_id:uuid}/keys/revoke",
|
|
1221
|
+
client_key_revoke,
|
|
1222
|
+
methods=["POST"],
|
|
1223
|
+
),
|
|
1224
|
+
Route(
|
|
1225
|
+
"/v1/admin/onboarding/{client_id:uuid}", client_onboarding, methods=["GET"]
|
|
1226
|
+
),
|
|
1227
|
+
Route("/v1/admin/audit", admin_audit, methods=["GET"]),
|
|
1228
|
+
# User management
|
|
1229
|
+
Route("/v1/admin/users", admin_users, methods=["GET", "POST"]),
|
|
1230
|
+
Route(
|
|
1231
|
+
"/v1/admin/users/{user_id:uuid}",
|
|
1232
|
+
user_detail,
|
|
1233
|
+
methods=["GET", "PATCH", "DELETE"],
|
|
1234
|
+
),
|
|
1235
|
+
# Workflow management
|
|
1236
|
+
Route("/v1/admin/workflows", admin_workflows, methods=["GET", "POST"]),
|
|
1237
|
+
Route(
|
|
1238
|
+
"/v1/admin/workflows/{workflow_id:uuid}",
|
|
1239
|
+
workflow_detail,
|
|
1240
|
+
methods=["GET", "PATCH", "DELETE"],
|
|
1241
|
+
),
|
|
1242
|
+
# Repository management
|
|
1243
|
+
Route("/v1/admin/repositories", admin_repositories, methods=["GET"]),
|
|
1244
|
+
Route(
|
|
1245
|
+
"/v1/admin/repositories/landscape", repository_landscape, methods=["GET"]
|
|
1246
|
+
),
|
|
1247
|
+
Route(
|
|
1248
|
+
"/v1/admin/repositories/{repo_id:uuid}",
|
|
1249
|
+
repository_detail,
|
|
1250
|
+
methods=["GET", "PATCH", "DELETE"],
|
|
1251
|
+
),
|
|
1252
|
+
Route(
|
|
1253
|
+
"/v1/admin/repositories/{repo_id:uuid}/branches",
|
|
1254
|
+
repository_branches,
|
|
1255
|
+
methods=["GET", "POST"],
|
|
1256
|
+
),
|
|
1257
|
+
Route(
|
|
1258
|
+
"/v1/admin/repositories/{repo_id:uuid}/branches/{branch:str}",
|
|
1259
|
+
repository_branch_delete,
|
|
1260
|
+
methods=["DELETE"],
|
|
1261
|
+
),
|
|
1262
|
+
Route(
|
|
1263
|
+
"/v1/admin/repositories/{repo_id:uuid}/branch-links",
|
|
1264
|
+
repository_branch_links,
|
|
1265
|
+
methods=["GET", "POST"],
|
|
1266
|
+
),
|
|
1267
|
+
Route(
|
|
1268
|
+
"/v1/admin/repositories/{repo_id:uuid}/branch-links/{link_id:str}",
|
|
1269
|
+
repository_branch_link_delete,
|
|
1270
|
+
methods=["DELETE"],
|
|
1271
|
+
),
|
|
1272
|
+
Route(
|
|
1273
|
+
"/v1/admin/repositories/{repo_id:uuid}/graph-map",
|
|
1274
|
+
repository_graph_map,
|
|
1275
|
+
methods=["GET"],
|
|
1276
|
+
),
|
|
1277
|
+
Route(
|
|
1278
|
+
"/v1/admin/repositories/{repo_id:uuid}/graph-summary",
|
|
1279
|
+
repository_graph_summary,
|
|
1280
|
+
methods=["GET"],
|
|
1281
|
+
),
|
|
1282
|
+
Route(
|
|
1283
|
+
"/v1/admin/repositories/{repo_id:uuid}/graph-search",
|
|
1284
|
+
repository_graph_search,
|
|
1285
|
+
methods=["GET"],
|
|
1286
|
+
),
|
|
1287
|
+
Route(
|
|
1288
|
+
"/v1/admin/repositories/{repo_id:uuid}/graph-impact",
|
|
1289
|
+
repository_graph_impact,
|
|
1290
|
+
methods=["GET"],
|
|
1291
|
+
),
|
|
1292
|
+
Route(
|
|
1293
|
+
"/v1/admin/repositories/{repo_id:uuid}/graph-sync",
|
|
1294
|
+
repository_graph_sync,
|
|
1295
|
+
methods=["POST"],
|
|
1296
|
+
),
|
|
1297
|
+
Route(
|
|
1298
|
+
"/v1/client/repositories/resolve",
|
|
1299
|
+
client_repository_resolve,
|
|
1300
|
+
methods=["POST"],
|
|
1301
|
+
),
|
|
1302
|
+
Route(
|
|
1303
|
+
"/v1/client/repositories/{repo_id:uuid}/graph-sync",
|
|
1304
|
+
client_repository_graph_sync,
|
|
1305
|
+
methods=["POST"],
|
|
1306
|
+
),
|
|
1307
|
+
# Observability
|
|
1308
|
+
Route("/v1/admin/metrics-summary", admin_metrics_summary, methods=["GET"]),
|
|
1309
|
+
]
|