dbl-gateway 0.3.2__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.
- dbl_gateway/__init__.py +1 -0
- dbl_gateway/adapters/__init__.py +9 -0
- dbl_gateway/adapters/execution_adapter_kl.py +133 -0
- dbl_gateway/adapters/policy_adapter_dbl_policy.py +96 -0
- dbl_gateway/adapters/store_adapter_sqlite.py +55 -0
- dbl_gateway/admission.py +67 -0
- dbl_gateway/app.py +501 -0
- dbl_gateway/auth.py +295 -0
- dbl_gateway/capabilities.py +79 -0
- dbl_gateway/digest.py +31 -0
- dbl_gateway/execution.py +15 -0
- dbl_gateway/governance.py +20 -0
- dbl_gateway/models.py +24 -0
- dbl_gateway/ports/__init__.py +11 -0
- dbl_gateway/ports/execution_port.py +19 -0
- dbl_gateway/ports/policy_port.py +18 -0
- dbl_gateway/ports/store_port.py +33 -0
- dbl_gateway/projection.py +34 -0
- dbl_gateway/providers/__init__.py +1 -0
- dbl_gateway/providers/anthropic.py +63 -0
- dbl_gateway/providers/errors.py +5 -0
- dbl_gateway/providers/openai.py +105 -0
- dbl_gateway/store/__init__.py +1 -0
- dbl_gateway/store/base.py +35 -0
- dbl_gateway/store/factory.py +12 -0
- dbl_gateway/store/sqlite.py +200 -0
- dbl_gateway/wire_contract.py +65 -0
- dbl_gateway-0.3.2.dist-info/METADATA +78 -0
- dbl_gateway-0.3.2.dist-info/RECORD +33 -0
- dbl_gateway-0.3.2.dist-info/WHEEL +5 -0
- dbl_gateway-0.3.2.dist-info/entry_points.txt +2 -0
- dbl_gateway-0.3.2.dist-info/licenses/LICENSE +21 -0
- dbl_gateway-0.3.2.dist-info/top_level.txt +1 -0
dbl_gateway/app.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Any, Mapping
|
|
11
|
+
|
|
12
|
+
from fastapi import Body, FastAPI, HTTPException, Query, Request
|
|
13
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
14
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
|
15
|
+
from dbl_core.normalize.trace import sanitize_trace
|
|
16
|
+
from dbl_core.events.trace_digest import trace_digest
|
|
17
|
+
|
|
18
|
+
from .admission import admit_and_shape_intent, AdmissionFailure
|
|
19
|
+
from .capabilities import get_capabilities
|
|
20
|
+
from .adapters.execution_adapter_kl import KlExecutionAdapter, schedule_execution
|
|
21
|
+
from .adapters.policy_adapter_dbl_policy import DblPolicyAdapter
|
|
22
|
+
from .ports.execution_port import ExecutionResult
|
|
23
|
+
from .ports.policy_port import DecisionResult
|
|
24
|
+
from .models import EventRecord
|
|
25
|
+
from .projection import project_runner_state, state_payload
|
|
26
|
+
from .store.factory import create_store
|
|
27
|
+
from .wire_contract import parse_intent_envelope
|
|
28
|
+
from .auth import (
|
|
29
|
+
Actor,
|
|
30
|
+
AuthError,
|
|
31
|
+
ForbiddenError,
|
|
32
|
+
authenticate_request,
|
|
33
|
+
require_roles,
|
|
34
|
+
require_tenant,
|
|
35
|
+
load_auth_config,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_LOGGER = logging.getLogger("dbl_gateway")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _configure_logging() -> None:
|
|
43
|
+
if _LOGGER.handlers:
|
|
44
|
+
return
|
|
45
|
+
handler = logging.StreamHandler()
|
|
46
|
+
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
47
|
+
_LOGGER.addHandler(handler)
|
|
48
|
+
_LOGGER.setLevel(logging.INFO)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_app() -> FastAPI:
|
|
52
|
+
@asynccontextmanager
|
|
53
|
+
async def lifespan(app: FastAPI):
|
|
54
|
+
_configure_logging()
|
|
55
|
+
app.state.store = create_store()
|
|
56
|
+
app.state.policy = DblPolicyAdapter()
|
|
57
|
+
app.state.execution = KlExecutionAdapter()
|
|
58
|
+
app.state.start_time = time.monotonic()
|
|
59
|
+
try:
|
|
60
|
+
yield
|
|
61
|
+
finally:
|
|
62
|
+
app.state.store.close()
|
|
63
|
+
|
|
64
|
+
app = FastAPI(title="DBL Gateway", lifespan=lifespan)
|
|
65
|
+
app.add_middleware(
|
|
66
|
+
CORSMiddleware,
|
|
67
|
+
allow_origins=["http://127.0.0.1:8787", "http://localhost:8787"],
|
|
68
|
+
allow_credentials=True,
|
|
69
|
+
allow_methods=["*"],
|
|
70
|
+
allow_headers=["*"],
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
@app.middleware("http")
|
|
74
|
+
async def request_logging(request: Request, call_next):
|
|
75
|
+
request_id = request.headers.get("x-request-id", "").strip() or uuid.uuid4().hex
|
|
76
|
+
request.state.request_id = request_id
|
|
77
|
+
start = time.monotonic()
|
|
78
|
+
response = await call_next(request)
|
|
79
|
+
response.headers["x-request-id"] = request_id
|
|
80
|
+
latency_ms = int((time.monotonic() - start) * 1000)
|
|
81
|
+
_LOGGER.info(
|
|
82
|
+
'{"message":"request.completed","request_id":"%s","method":"%s","path":"%s","status_code":%d,"latency_ms":%d}',
|
|
83
|
+
request_id,
|
|
84
|
+
request.method,
|
|
85
|
+
request.url.path,
|
|
86
|
+
response.status_code,
|
|
87
|
+
latency_ms,
|
|
88
|
+
)
|
|
89
|
+
return response
|
|
90
|
+
|
|
91
|
+
@app.get("/healthz")
|
|
92
|
+
async def healthz() -> dict[str, str]:
|
|
93
|
+
return {"status": "ok"}
|
|
94
|
+
|
|
95
|
+
@app.get("/capabilities")
|
|
96
|
+
async def capabilities(request: Request) -> dict[str, object]:
|
|
97
|
+
actor = await _require_actor(request)
|
|
98
|
+
_require_role(actor, ["gateway.snapshot.read"])
|
|
99
|
+
return get_capabilities()
|
|
100
|
+
|
|
101
|
+
@app.post("/ingress/intent")
|
|
102
|
+
async def ingress_intent(request: Request, body: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
|
103
|
+
actor = await _require_actor(request)
|
|
104
|
+
_require_role(actor, ["gateway.intent.write"])
|
|
105
|
+
try:
|
|
106
|
+
envelope = parse_intent_envelope(body)
|
|
107
|
+
except ValueError as exc:
|
|
108
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
109
|
+
|
|
110
|
+
trace_id = uuid.uuid4().hex
|
|
111
|
+
intent_payload = envelope["payload"]
|
|
112
|
+
raw_payload = intent_payload["payload"]
|
|
113
|
+
shaped_payload = _shape_payload(intent_payload["intent_type"], raw_payload)
|
|
114
|
+
try:
|
|
115
|
+
admission_record = admit_and_shape_intent(
|
|
116
|
+
{
|
|
117
|
+
"correlation_id": envelope["correlation_id"],
|
|
118
|
+
"deterministic": {
|
|
119
|
+
"stream_id": intent_payload["stream_id"],
|
|
120
|
+
"lane": intent_payload["lane"],
|
|
121
|
+
"actor": intent_payload["actor"],
|
|
122
|
+
"intent_type": intent_payload["intent_type"],
|
|
123
|
+
"payload": shaped_payload,
|
|
124
|
+
},
|
|
125
|
+
"observational": {},
|
|
126
|
+
},
|
|
127
|
+
raw_payload=raw_payload,
|
|
128
|
+
)
|
|
129
|
+
except AdmissionFailure as exc:
|
|
130
|
+
return JSONResponse(
|
|
131
|
+
status_code=400,
|
|
132
|
+
content={"ok": False, "reason_code": exc.reason_code, "detail": exc.detail},
|
|
133
|
+
)
|
|
134
|
+
authoritative = _thaw_json(admission_record.deterministic)
|
|
135
|
+
authoritative["correlation_id"] = admission_record.correlation_id
|
|
136
|
+
if intent_payload.get("requested_model_id"):
|
|
137
|
+
authoritative["payload"]["requested_model_id"] = intent_payload["requested_model_id"]
|
|
138
|
+
_attach_obs_trace_id(authoritative["payload"], trace_id)
|
|
139
|
+
intent_event = app.state.store.append(
|
|
140
|
+
kind="INTENT",
|
|
141
|
+
lane=authoritative["lane"],
|
|
142
|
+
actor=authoritative["actor"],
|
|
143
|
+
intent_type=authoritative["intent_type"],
|
|
144
|
+
stream_id=authoritative["stream_id"],
|
|
145
|
+
correlation_id=envelope["correlation_id"],
|
|
146
|
+
payload=authoritative["payload"],
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
decision = app.state.policy.decide(authoritative)
|
|
150
|
+
decision_event = app.state.store.append(
|
|
151
|
+
kind="DECISION",
|
|
152
|
+
lane=authoritative["lane"],
|
|
153
|
+
actor="policy",
|
|
154
|
+
intent_type=authoritative["intent_type"],
|
|
155
|
+
stream_id=authoritative["stream_id"],
|
|
156
|
+
correlation_id=envelope["correlation_id"],
|
|
157
|
+
payload=_decision_payload(decision, trace_id),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if decision.decision == "ALLOW" and _get_exec_mode() == "embedded":
|
|
161
|
+
schedule_execution(_execute_background(app, intent_event, envelope["correlation_id"]))
|
|
162
|
+
|
|
163
|
+
return JSONResponse(
|
|
164
|
+
status_code=202,
|
|
165
|
+
content={
|
|
166
|
+
"ok": True,
|
|
167
|
+
"index": intent_event["index"],
|
|
168
|
+
"correlation_id": envelope["correlation_id"],
|
|
169
|
+
"stream_id": authoritative["stream_id"],
|
|
170
|
+
"lane": authoritative["lane"],
|
|
171
|
+
"intent_type": authoritative["intent_type"],
|
|
172
|
+
"accepted_at": datetime.now(timezone.utc).isoformat(),
|
|
173
|
+
"expected_next": "DECISION",
|
|
174
|
+
},
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
@app.get("/snapshot")
|
|
178
|
+
async def snapshot(
|
|
179
|
+
request: Request,
|
|
180
|
+
limit: int = Query(200, ge=1, le=2000),
|
|
181
|
+
offset: int = Query(0, ge=0),
|
|
182
|
+
stream_id: str | None = Query(None),
|
|
183
|
+
lane: str | None = Query(None),
|
|
184
|
+
) -> dict[str, Any]:
|
|
185
|
+
actor = await _require_actor(request)
|
|
186
|
+
_require_role(actor, ["gateway.snapshot.read"])
|
|
187
|
+
return app.state.store.snapshot(
|
|
188
|
+
limit=limit,
|
|
189
|
+
offset=offset,
|
|
190
|
+
stream_id=_normalize_optional_str(stream_id, "stream_id"),
|
|
191
|
+
lane=_normalize_optional_str(lane, "lane"),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
@app.get("/tail")
|
|
195
|
+
async def tail(
|
|
196
|
+
request: Request,
|
|
197
|
+
stream_id: str | None = Query(None),
|
|
198
|
+
since: int = Query(0, ge=0),
|
|
199
|
+
lanes: str | None = Query(None),
|
|
200
|
+
) -> StreamingResponse:
|
|
201
|
+
actor = await _require_actor(request)
|
|
202
|
+
_require_role(actor, ["gateway.snapshot.read"])
|
|
203
|
+
|
|
204
|
+
last_event_id = request.headers.get("last-event-id")
|
|
205
|
+
if last_event_id and last_event_id.isdigit():
|
|
206
|
+
since = max(since, int(last_event_id) + 1)
|
|
207
|
+
|
|
208
|
+
lane_filter: set[str] | None = None
|
|
209
|
+
if lanes:
|
|
210
|
+
lane_filter = {lane.strip() for lane in lanes.split(",") if lane.strip()}
|
|
211
|
+
if not lane_filter:
|
|
212
|
+
lane_filter = None
|
|
213
|
+
|
|
214
|
+
async def event_stream():
|
|
215
|
+
cursor = since
|
|
216
|
+
while True:
|
|
217
|
+
if await request.is_disconnected():
|
|
218
|
+
break
|
|
219
|
+
snap = app.state.store.snapshot(
|
|
220
|
+
limit=2000,
|
|
221
|
+
offset=cursor,
|
|
222
|
+
stream_id=_normalize_optional_str(stream_id, "stream_id") if stream_id else None,
|
|
223
|
+
)
|
|
224
|
+
events = snap.get("events", [])
|
|
225
|
+
if not events:
|
|
226
|
+
await asyncio.sleep(0.5)
|
|
227
|
+
continue
|
|
228
|
+
max_index = cursor - 1
|
|
229
|
+
for event in events:
|
|
230
|
+
idx = event.get("index")
|
|
231
|
+
if isinstance(idx, int) and idx > max_index:
|
|
232
|
+
max_index = idx
|
|
233
|
+
if lane_filter and event.get("lane") not in lane_filter:
|
|
234
|
+
continue
|
|
235
|
+
data = json.dumps(event, ensure_ascii=True, separators=(",", ":"))
|
|
236
|
+
event_id = str(idx) if isinstance(idx, int) else ""
|
|
237
|
+
if event_id:
|
|
238
|
+
yield f"id: {event_id}\n"
|
|
239
|
+
yield "event: envelope\n"
|
|
240
|
+
yield f"data: {data}\n\n"
|
|
241
|
+
if max_index >= cursor:
|
|
242
|
+
cursor = max_index + 1
|
|
243
|
+
|
|
244
|
+
headers = {
|
|
245
|
+
"Cache-Control": "no-cache",
|
|
246
|
+
"Connection": "keep-alive",
|
|
247
|
+
}
|
|
248
|
+
return StreamingResponse(event_stream(), media_type="text/event-stream", headers=headers)
|
|
249
|
+
|
|
250
|
+
@app.get("/status")
|
|
251
|
+
async def status_surface(
|
|
252
|
+
request: Request,
|
|
253
|
+
stream_id: str | None = Query(None),
|
|
254
|
+
) -> dict[str, object]:
|
|
255
|
+
actor = await _require_actor(request)
|
|
256
|
+
_require_role(actor, ["gateway.snapshot.read"])
|
|
257
|
+
snap = app.state.store.snapshot(limit=2000, offset=0, stream_id=stream_id)
|
|
258
|
+
state = project_runner_state(snap["events"])
|
|
259
|
+
return state_payload(state)
|
|
260
|
+
|
|
261
|
+
@app.post("/execution/event")
|
|
262
|
+
async def execution_event(request: Request, body: dict[str, Any] = Body(...)) -> dict[str, Any]:
|
|
263
|
+
actor = await _require_actor(request)
|
|
264
|
+
_require_role(actor, ["gateway.execution.write"])
|
|
265
|
+
if _get_exec_mode() != "external":
|
|
266
|
+
raise HTTPException(status_code=403, detail="execution events disabled in embedded mode")
|
|
267
|
+
correlation_id = body.get("correlation_id")
|
|
268
|
+
payload = body.get("payload")
|
|
269
|
+
if not isinstance(correlation_id, str) or not correlation_id:
|
|
270
|
+
raise HTTPException(status_code=400, detail="correlation_id must be a non-empty string")
|
|
271
|
+
if not isinstance(payload, Mapping):
|
|
272
|
+
raise HTTPException(status_code=400, detail="payload must be an object")
|
|
273
|
+
lane = str(body.get("lane", ""))
|
|
274
|
+
actor = str(body.get("actor", ""))
|
|
275
|
+
intent_type = str(body.get("intent_type", ""))
|
|
276
|
+
stream_id = str(body.get("stream_id", ""))
|
|
277
|
+
if not all([lane, actor, intent_type, stream_id]):
|
|
278
|
+
raise HTTPException(status_code=400, detail="lane, actor, intent_type, stream_id required")
|
|
279
|
+
if not _decision_allows_execution(app, correlation_id):
|
|
280
|
+
raise HTTPException(status_code=409, detail="no ALLOW decision for correlation_id")
|
|
281
|
+
p = dict(payload)
|
|
282
|
+
trace_value = p.get("trace")
|
|
283
|
+
if isinstance(trace_value, Mapping):
|
|
284
|
+
trace, trace_digest_value = make_trace_bundle(trace_value)
|
|
285
|
+
else:
|
|
286
|
+
trace, trace_digest_value = make_trace_bundle(
|
|
287
|
+
{
|
|
288
|
+
"trace_id": correlation_id,
|
|
289
|
+
"lane": lane,
|
|
290
|
+
"intent_type": intent_type,
|
|
291
|
+
"stream_id": stream_id,
|
|
292
|
+
}
|
|
293
|
+
)
|
|
294
|
+
p["trace"] = trace
|
|
295
|
+
p["trace_digest"] = trace_digest_value
|
|
296
|
+
event = app.state.store.append(
|
|
297
|
+
kind="EXECUTION",
|
|
298
|
+
lane=lane,
|
|
299
|
+
actor=actor,
|
|
300
|
+
intent_type=intent_type,
|
|
301
|
+
stream_id=stream_id,
|
|
302
|
+
correlation_id=correlation_id,
|
|
303
|
+
payload=p,
|
|
304
|
+
)
|
|
305
|
+
return {"ok": True, "execution_index": event["index"]}
|
|
306
|
+
|
|
307
|
+
return app
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
async def _execute_background(app: FastAPI, intent_event: EventRecord, correlation_id: str) -> None:
|
|
311
|
+
trace_id = _extract_trace_id(intent_event)
|
|
312
|
+
try:
|
|
313
|
+
result = await app.state.execution.run(intent_event)
|
|
314
|
+
payload = _execution_payload(result, trace_id)
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
requested_model = ""
|
|
317
|
+
intent_payload = intent_event.get("payload")
|
|
318
|
+
if isinstance(intent_payload, Mapping):
|
|
319
|
+
requested_model = str(intent_payload.get("requested_model_id", "") or "")
|
|
320
|
+
trace, trace_digest_value = make_trace_bundle(
|
|
321
|
+
{
|
|
322
|
+
"trace_id": trace_id,
|
|
323
|
+
"error_type": type(exc).__name__,
|
|
324
|
+
"error": str(exc),
|
|
325
|
+
"lane": intent_event.get("lane"),
|
|
326
|
+
"actor": intent_event.get("actor"),
|
|
327
|
+
"intent_type": intent_event.get("intent_type"),
|
|
328
|
+
"stream_id": intent_event.get("stream_id"),
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
payload = {
|
|
332
|
+
"provider": "kl",
|
|
333
|
+
"model_id": requested_model or "unknown",
|
|
334
|
+
"error": f"{type(exc).__name__}: {exc}",
|
|
335
|
+
"trace": trace,
|
|
336
|
+
"trace_digest": trace_digest_value,
|
|
337
|
+
}
|
|
338
|
+
app.state.store.append(
|
|
339
|
+
kind="EXECUTION",
|
|
340
|
+
lane=intent_event["lane"],
|
|
341
|
+
actor="executor",
|
|
342
|
+
intent_type=intent_event["intent_type"],
|
|
343
|
+
stream_id=intent_event["stream_id"],
|
|
344
|
+
correlation_id=correlation_id,
|
|
345
|
+
payload=payload,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _decision_payload(decision: DecisionResult, trace_id: str) -> dict[str, Any]:
|
|
350
|
+
payload: dict[str, Any] = {
|
|
351
|
+
"decision": decision.decision,
|
|
352
|
+
"reason_codes": decision.reason_codes,
|
|
353
|
+
}
|
|
354
|
+
if decision.policy_id:
|
|
355
|
+
payload["policy_id"] = decision.policy_id
|
|
356
|
+
if decision.policy_version is not None:
|
|
357
|
+
payload["policy_version"] = decision.policy_version
|
|
358
|
+
_attach_obs_trace_id(payload, trace_id)
|
|
359
|
+
return payload
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _execution_payload(result: ExecutionResult, trace_id: str) -> dict[str, Any]:
|
|
363
|
+
payload: dict[str, Any] = {
|
|
364
|
+
"provider": result.provider,
|
|
365
|
+
"model_id": result.model_id,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if result.error:
|
|
369
|
+
payload["error"] = result.error
|
|
370
|
+
else:
|
|
371
|
+
payload["output_text"] = result.output_text or ""
|
|
372
|
+
|
|
373
|
+
if isinstance(result.trace, Mapping):
|
|
374
|
+
raw_trace = dict(result.trace)
|
|
375
|
+
raw_trace.setdefault("trace_id", trace_id)
|
|
376
|
+
elif result.trace is not None:
|
|
377
|
+
raw_trace = {"trace_id": trace_id, "value": result.trace}
|
|
378
|
+
else:
|
|
379
|
+
raw_trace = {
|
|
380
|
+
"trace_id": trace_id,
|
|
381
|
+
"provider": result.provider,
|
|
382
|
+
"model_id": result.model_id,
|
|
383
|
+
"has_error": bool(result.error),
|
|
384
|
+
}
|
|
385
|
+
trace, trace_digest_value = make_trace_bundle(raw_trace)
|
|
386
|
+
payload["trace"] = trace
|
|
387
|
+
payload["trace_digest"] = trace_digest_value
|
|
388
|
+
return payload
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _decision_allows_execution(app: FastAPI, correlation_id: str) -> bool:
|
|
392
|
+
snap = app.state.store.snapshot(limit=2000, offset=0)
|
|
393
|
+
events = [e for e in snap["events"] if e["correlation_id"] == correlation_id]
|
|
394
|
+
for event in reversed(events):
|
|
395
|
+
if event["kind"] == "DECISION":
|
|
396
|
+
payload = event["payload"]
|
|
397
|
+
if isinstance(payload, Mapping):
|
|
398
|
+
return payload.get("decision") == "ALLOW"
|
|
399
|
+
return False
|
|
400
|
+
return False
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _normalize_optional_str(value: str | None, name: str) -> str | None:
|
|
404
|
+
if value is None:
|
|
405
|
+
return None
|
|
406
|
+
if value.strip() == "":
|
|
407
|
+
raise HTTPException(status_code=400, detail=f"{name} must be a non-empty string")
|
|
408
|
+
return value.strip()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _get_exec_mode() -> str:
|
|
412
|
+
import os
|
|
413
|
+
|
|
414
|
+
return os.getenv("GATEWAY_EXEC_MODE", "embedded").strip().lower()
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
async def _require_actor(request: Request) -> Actor:
|
|
418
|
+
cfg = load_auth_config()
|
|
419
|
+
try:
|
|
420
|
+
actor = await authenticate_request(request.headers, cfg)
|
|
421
|
+
require_tenant(actor, cfg)
|
|
422
|
+
return actor
|
|
423
|
+
except AuthError as exc:
|
|
424
|
+
raise HTTPException(status_code=401, detail=str(exc)) from exc
|
|
425
|
+
except ForbiddenError as exc:
|
|
426
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _require_role(actor: Actor, roles: list[str]) -> None:
|
|
430
|
+
try:
|
|
431
|
+
require_roles(actor, roles)
|
|
432
|
+
except ForbiddenError as exc:
|
|
433
|
+
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _attach_obs_trace_id(payload: dict[str, Any], trace_id: str) -> None:
|
|
437
|
+
obs = payload.get("_obs")
|
|
438
|
+
if not isinstance(obs, dict):
|
|
439
|
+
obs = {}
|
|
440
|
+
payload["_obs"] = obs
|
|
441
|
+
obs["trace_id"] = trace_id
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _shape_payload(intent_type: str, payload: Mapping[str, Any]) -> dict[str, Any]:
|
|
445
|
+
if intent_type == "chat.message":
|
|
446
|
+
shaped: dict[str, Any] = {}
|
|
447
|
+
message = payload.get("message")
|
|
448
|
+
if isinstance(message, str):
|
|
449
|
+
shaped["message"] = message
|
|
450
|
+
client_msg_id = payload.get("client_msg_id")
|
|
451
|
+
if isinstance(client_msg_id, str) and client_msg_id.strip():
|
|
452
|
+
shaped["client_msg_id"] = client_msg_id
|
|
453
|
+
return shaped
|
|
454
|
+
return dict(payload)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _thaw_json(value: Any) -> Any:
|
|
458
|
+
if isinstance(value, Mapping):
|
|
459
|
+
return {str(k): _thaw_json(v) for k, v in value.items()}
|
|
460
|
+
if isinstance(value, tuple):
|
|
461
|
+
return [_thaw_json(item) for item in value]
|
|
462
|
+
return value
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _extract_trace_id(intent_event: EventRecord) -> str:
|
|
466
|
+
payload = intent_event.get("payload")
|
|
467
|
+
if isinstance(payload, Mapping):
|
|
468
|
+
obs = payload.get("_obs")
|
|
469
|
+
if isinstance(obs, Mapping):
|
|
470
|
+
trace_id = obs.get("trace_id")
|
|
471
|
+
if isinstance(trace_id, str) and trace_id.strip():
|
|
472
|
+
return trace_id
|
|
473
|
+
return uuid.uuid4().hex
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def make_trace_bundle(raw_trace: Mapping[str, Any]) -> tuple[dict[str, Any], str]:
|
|
477
|
+
trace = sanitize_trace(raw_trace)
|
|
478
|
+
return trace, trace_digest(trace)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def main() -> None:
|
|
482
|
+
import argparse
|
|
483
|
+
import uvicorn
|
|
484
|
+
|
|
485
|
+
parser = argparse.ArgumentParser(prog="dbl-gateway")
|
|
486
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
487
|
+
serve = sub.add_parser("serve")
|
|
488
|
+
serve.add_argument("--host", default="127.0.0.1")
|
|
489
|
+
serve.add_argument("--port", type=int, default=8010)
|
|
490
|
+
serve.add_argument("--db", default=".\\data\\trail.sqlite")
|
|
491
|
+
args = parser.parse_args()
|
|
492
|
+
|
|
493
|
+
if args.db:
|
|
494
|
+
import os
|
|
495
|
+
|
|
496
|
+
os.environ["DBL_GATEWAY_DB"] = str(args.db)
|
|
497
|
+
app = create_app()
|
|
498
|
+
uvicorn.run(app, host=args.host, port=args.port, reload=False)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
app = create_app()
|