dbl-gateway 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. dbl_gateway-0.3.2/LICENSE +21 -0
  2. dbl_gateway-0.3.2/PKG-INFO +78 -0
  3. dbl_gateway-0.3.2/README.md +53 -0
  4. dbl_gateway-0.3.2/pyproject.toml +53 -0
  5. dbl_gateway-0.3.2/setup.cfg +4 -0
  6. dbl_gateway-0.3.2/src/dbl_gateway/__init__.py +1 -0
  7. dbl_gateway-0.3.2/src/dbl_gateway/adapters/__init__.py +9 -0
  8. dbl_gateway-0.3.2/src/dbl_gateway/adapters/execution_adapter_kl.py +133 -0
  9. dbl_gateway-0.3.2/src/dbl_gateway/adapters/policy_adapter_dbl_policy.py +96 -0
  10. dbl_gateway-0.3.2/src/dbl_gateway/adapters/store_adapter_sqlite.py +55 -0
  11. dbl_gateway-0.3.2/src/dbl_gateway/admission.py +67 -0
  12. dbl_gateway-0.3.2/src/dbl_gateway/app.py +501 -0
  13. dbl_gateway-0.3.2/src/dbl_gateway/auth.py +295 -0
  14. dbl_gateway-0.3.2/src/dbl_gateway/capabilities.py +79 -0
  15. dbl_gateway-0.3.2/src/dbl_gateway/digest.py +31 -0
  16. dbl_gateway-0.3.2/src/dbl_gateway/execution.py +15 -0
  17. dbl_gateway-0.3.2/src/dbl_gateway/governance.py +20 -0
  18. dbl_gateway-0.3.2/src/dbl_gateway/models.py +24 -0
  19. dbl_gateway-0.3.2/src/dbl_gateway/ports/__init__.py +11 -0
  20. dbl_gateway-0.3.2/src/dbl_gateway/ports/execution_port.py +19 -0
  21. dbl_gateway-0.3.2/src/dbl_gateway/ports/policy_port.py +18 -0
  22. dbl_gateway-0.3.2/src/dbl_gateway/ports/store_port.py +33 -0
  23. dbl_gateway-0.3.2/src/dbl_gateway/projection.py +34 -0
  24. dbl_gateway-0.3.2/src/dbl_gateway/providers/__init__.py +1 -0
  25. dbl_gateway-0.3.2/src/dbl_gateway/providers/anthropic.py +63 -0
  26. dbl_gateway-0.3.2/src/dbl_gateway/providers/errors.py +5 -0
  27. dbl_gateway-0.3.2/src/dbl_gateway/providers/openai.py +105 -0
  28. dbl_gateway-0.3.2/src/dbl_gateway/store/__init__.py +1 -0
  29. dbl_gateway-0.3.2/src/dbl_gateway/store/base.py +35 -0
  30. dbl_gateway-0.3.2/src/dbl_gateway/store/factory.py +12 -0
  31. dbl_gateway-0.3.2/src/dbl_gateway/store/sqlite.py +200 -0
  32. dbl_gateway-0.3.2/src/dbl_gateway/wire_contract.py +65 -0
  33. dbl_gateway-0.3.2/src/dbl_gateway.egg-info/PKG-INFO +78 -0
  34. dbl_gateway-0.3.2/src/dbl_gateway.egg-info/SOURCES.txt +40 -0
  35. dbl_gateway-0.3.2/src/dbl_gateway.egg-info/dependency_links.txt +1 -0
  36. dbl_gateway-0.3.2/src/dbl_gateway.egg-info/entry_points.txt +2 -0
  37. dbl_gateway-0.3.2/src/dbl_gateway.egg-info/requires.txt +17 -0
  38. dbl_gateway-0.3.2/src/dbl_gateway.egg-info/top_level.txt +1 -0
  39. dbl_gateway-0.3.2/tests/test_dependency_imports.py +20 -0
  40. dbl_gateway-0.3.2/tests/test_gateway.py +405 -0
  41. dbl_gateway-0.3.2/tests/test_reference_oracle.py +123 -0
  42. dbl_gateway-0.3.2/tests/test_schema_snapshot.py +15 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lukas Pfister
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,78 @@
1
+ Metadata-Version: 2.4
2
+ Name: dbl-gateway
3
+ Version: 0.3.2
4
+ Summary: DBL Gateway
5
+ License: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: dbl-core<0.4.0,>=0.3.2
10
+ Requires-Dist: dbl-policy<0.2.0,>=0.1.0
11
+ Requires-Dist: dbl-main<0.4.0,>=0.3.0
12
+ Requires-Dist: dbl-ingress<0.2.0,>=0.1.1
13
+ Requires-Dist: kl-kernel-logic<0.6.0,>=0.5.0
14
+ Requires-Dist: fastapi>=0.110.0
15
+ Requires-Dist: uvicorn>=0.27.0
16
+ Requires-Dist: httpx>=0.27.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest<9,>=8; extra == "dev"
19
+ Requires-Dist: ruff<1,>=0.5; extra == "dev"
20
+ Requires-Dist: mypy<2,>=1.10; extra == "dev"
21
+ Requires-Dist: dbl-reference>=0.2.0; extra == "dev"
22
+ Provides-Extra: oidc
23
+ Requires-Dist: python-jose[cryptography]<4,>=3.3; extra == "oidc"
24
+ Dynamic: license-file
25
+
26
+ # DBL Gateway 0.3.2
27
+
28
+ Authoritative DBL and KL gateway. This service is the single writer for append-only trails,
29
+ applies policy via dbl-policy, and executes via kl-kernel-logic. UI and boundary services
30
+ consume its snapshots and emit INTENT only.
31
+
32
+ This release stabilizes the 0.3.x stackline and does not introduce new wire contracts.
33
+
34
+ Compatible stack versions:
35
+ - dbl-core==0.3.2
36
+ - dbl-policy==0.1.0
37
+ - dbl-main==0.3.0
38
+ - kl-kernel-logic==0.5.0
39
+
40
+ ## Quickstart (PowerShell)
41
+
42
+ ```powershell
43
+ py -3.11 -m venv .venv
44
+ .venv\Scripts\Activate.ps1
45
+ python -m pip install -e ".[dev]"
46
+ ```
47
+
48
+ Run the gateway:
49
+ ```powershell
50
+ dbl-gateway serve --db .\data\trail.sqlite --host 127.0.0.1 --port 8010
51
+ ```
52
+
53
+ Run with uvicorn:
54
+ ```powershell
55
+ $env:DBL_GATEWAY_DB=".\data\trail.sqlite"
56
+ py -3.11 -m uvicorn dbl_gateway.app:app --host 127.0.0.1 --port 8010
57
+ ```
58
+
59
+ ## Endpoints
60
+
61
+ Write:
62
+ - POST `/ingress/intent`
63
+
64
+ Read:
65
+ - GET `/snapshot`
66
+ - GET `/capabilities`
67
+ - GET `/healthz`
68
+
69
+ ## Environment contract
70
+
71
+ See `docs/env_contract.md`.
72
+
73
+ ## Notes
74
+
75
+ - The gateway is the only component that performs governance and execution.
76
+ - All stabilization is expressed explicitly via DECISION events.
77
+ - Boundary and UI clients do not import dbl-core or dbl-policy.
78
+ - The gateway uses dbl-core for canonicalization and digest computation.
@@ -0,0 +1,53 @@
1
+ # DBL Gateway 0.3.2
2
+
3
+ Authoritative DBL and KL gateway. This service is the single writer for append-only trails,
4
+ applies policy via dbl-policy, and executes via kl-kernel-logic. UI and boundary services
5
+ consume its snapshots and emit INTENT only.
6
+
7
+ This release stabilizes the 0.3.x stackline and does not introduce new wire contracts.
8
+
9
+ Compatible stack versions:
10
+ - dbl-core==0.3.2
11
+ - dbl-policy==0.1.0
12
+ - dbl-main==0.3.0
13
+ - kl-kernel-logic==0.5.0
14
+
15
+ ## Quickstart (PowerShell)
16
+
17
+ ```powershell
18
+ py -3.11 -m venv .venv
19
+ .venv\Scripts\Activate.ps1
20
+ python -m pip install -e ".[dev]"
21
+ ```
22
+
23
+ Run the gateway:
24
+ ```powershell
25
+ dbl-gateway serve --db .\data\trail.sqlite --host 127.0.0.1 --port 8010
26
+ ```
27
+
28
+ Run with uvicorn:
29
+ ```powershell
30
+ $env:DBL_GATEWAY_DB=".\data\trail.sqlite"
31
+ py -3.11 -m uvicorn dbl_gateway.app:app --host 127.0.0.1 --port 8010
32
+ ```
33
+
34
+ ## Endpoints
35
+
36
+ Write:
37
+ - POST `/ingress/intent`
38
+
39
+ Read:
40
+ - GET `/snapshot`
41
+ - GET `/capabilities`
42
+ - GET `/healthz`
43
+
44
+ ## Environment contract
45
+
46
+ See `docs/env_contract.md`.
47
+
48
+ ## Notes
49
+
50
+ - The gateway is the only component that performs governance and execution.
51
+ - All stabilization is expressed explicitly via DECISION events.
52
+ - Boundary and UI clients do not import dbl-core or dbl-policy.
53
+ - The gateway uses dbl-core for canonicalization and digest computation.
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "dbl-gateway"
7
+ version = "0.3.2"
8
+ description = "DBL Gateway"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+
13
+ dependencies = [
14
+ "dbl-core>=0.3.2,<0.4.0",
15
+ "dbl-policy>=0.1.0,<0.2.0",
16
+ "dbl-main>=0.3.0,<0.4.0",
17
+ "dbl-ingress>=0.1.1,<0.2.0",
18
+ "kl-kernel-logic>=0.5.0,<0.6.0",
19
+ "fastapi>=0.110.0",
20
+ "uvicorn>=0.27.0",
21
+ "httpx>=0.27.0",
22
+ ]
23
+
24
+ [project.scripts]
25
+ dbl-gateway = "dbl_gateway.app:main"
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "pytest>=8,<9",
30
+ "ruff>=0.5,<1",
31
+ "mypy>=1.10,<2",
32
+ "dbl-reference>=0.2.0",
33
+ ]
34
+ oidc = [
35
+ "python-jose[cryptography]>=3.3,<4",
36
+ ]
37
+
38
+ [tool.setuptools]
39
+ package-dir = { "" = "src" }
40
+ license-files = ["LICENSE"]
41
+
42
+ [tool.setuptools.packages.find]
43
+ where = ["src"]
44
+ include = ["dbl_gateway*"]
45
+ exclude = ["tests*", "docs*", "scripts*"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+ pythonpath = ["src"]
50
+ filterwarnings = [
51
+ "ignore:.*on_event is deprecated.*:DeprecationWarning",
52
+ ]
53
+ addopts = ["-q", "--strict-markers", "--strict-config"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __all__ = ["app"]
@@ -0,0 +1,9 @@
1
+ from .execution_adapter_kl import KlExecutionAdapter
2
+ from .policy_adapter_dbl_policy import DblPolicyAdapter
3
+ from .store_adapter_sqlite import SQLiteStoreAdapter
4
+
5
+ __all__ = [
6
+ "DblPolicyAdapter",
7
+ "KlExecutionAdapter",
8
+ "SQLiteStoreAdapter",
9
+ ]
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass
5
+ from typing import Any, Mapping
6
+
7
+ from dbl_core import normalize_trace
8
+
9
+ from ..ports.execution_port import ExecutionPort, ExecutionResult
10
+ from ..providers import anthropic, openai
11
+ from ..providers.errors import ProviderError
12
+ from ..capabilities import resolve_provider
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class KlExecutionAdapter(ExecutionPort):
17
+ async def run(self, intent_event: Mapping[str, Any]) -> ExecutionResult:
18
+ payload = intent_event.get("payload")
19
+ if not isinstance(payload, Mapping):
20
+ return ExecutionResult(error={"message": "invalid payload"})
21
+ requested_model_id = payload.get("requested_model_id")
22
+ model_id = str(requested_model_id) if requested_model_id else ""
23
+ provider, reason = resolve_provider(model_id)
24
+ if provider is None or reason is not None:
25
+ return ExecutionResult(
26
+ provider=provider,
27
+ model_id=model_id,
28
+ error={"provider": provider, "message": reason or "model.unavailable"},
29
+ )
30
+ message = _extract_message(payload)
31
+ if message is None:
32
+ return ExecutionResult(provider=provider, model_id=model_id, error={"message": "input.invalid"})
33
+ call = _select_provider(provider)
34
+ try:
35
+ output_text, trace, trace_digest, error = await _call_kernel(message, model_id, provider, call)
36
+ return ExecutionResult(
37
+ output_text=output_text,
38
+ provider=provider,
39
+ model_id=model_id,
40
+ trace=trace,
41
+ trace_digest=trace_digest,
42
+ error=error,
43
+ )
44
+ except Exception:
45
+ return ExecutionResult(
46
+ provider=provider,
47
+ model_id=model_id,
48
+ error={
49
+ "provider": provider,
50
+ "message": "execution failed",
51
+ },
52
+ )
53
+
54
+
55
+ def schedule_execution(coro: asyncio.Task | asyncio.Future | Any) -> None:
56
+ asyncio.create_task(coro)
57
+
58
+
59
+ def _select_provider(name: str):
60
+ if name == "openai":
61
+ return openai.execute
62
+ if name == "anthropic":
63
+ return anthropic.execute
64
+ raise RuntimeError("unsupported provider")
65
+
66
+
67
+ async def _call_kernel(message: str, model_id: str, provider: str, provider_call):
68
+ import kl_kernel_logic
69
+
70
+ psi = kl_kernel_logic.PsiDefinition(
71
+ psi_type="llm",
72
+ name=provider,
73
+ metadata={"model_id": model_id},
74
+ )
75
+ kernel = kl_kernel_logic.Kernel()
76
+
77
+ def _task(message: str, model_id: str) -> dict[str, object]:
78
+ try:
79
+ return {"ok": True, "output": provider_call(message, model_id)}
80
+ except ProviderError as exc:
81
+ return {
82
+ "ok": False,
83
+ "error": {
84
+ "status_code": exc.status_code,
85
+ "code": exc.code,
86
+ "message": str(exc),
87
+ },
88
+ }
89
+
90
+ trace = await asyncio.to_thread(
91
+ kernel.execute,
92
+ psi=psi,
93
+ task=_task,
94
+ metadata={"provider": provider, "model_id": model_id},
95
+ message=message,
96
+ model_id=model_id,
97
+ )
98
+ trace_dict, trace_digest_value = normalize_trace(trace)
99
+ if not trace.success:
100
+ return (
101
+ "",
102
+ trace_dict,
103
+ trace_digest_value,
104
+ {
105
+ "provider": provider,
106
+ "message": trace.error or "execution failed",
107
+ "failure_code": getattr(trace.failure_code, "value", None),
108
+ },
109
+ )
110
+ output = trace.output
111
+ if isinstance(output, dict) and output.get("ok") is False:
112
+ err = output.get("error") if isinstance(output.get("error"), dict) else {}
113
+ return (
114
+ "",
115
+ trace_dict,
116
+ trace_digest_value,
117
+ {
118
+ "provider": provider,
119
+ "status_code": err.get("status_code"),
120
+ "code": err.get("code"),
121
+ "message": str(err.get("message") or "execution failed"),
122
+ },
123
+ )
124
+ if isinstance(output, dict) and "output" in output:
125
+ return str(output.get("output") or ""), trace_dict, trace_digest_value, None
126
+ return str(output or ""), trace_dict, trace_digest_value, None
127
+
128
+
129
+ def _extract_message(payload: Mapping[str, Any]) -> str | None:
130
+ message = payload.get("message")
131
+ if isinstance(message, str) and message.strip():
132
+ return message.strip()
133
+ return None
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from importlib import import_module
5
+ from typing import Any, Mapping, get_type_hints
6
+
7
+ from dbl_policy import Policy, PolicyContext, PolicyDecision, decision_to_dbl_event
8
+
9
+ from ..ports.policy_port import DecisionResult, PolicyPort
10
+
11
+
12
+ ALLOWED_CONTEXT_KEYS = {
13
+ "stream_id",
14
+ "lane",
15
+ "actor",
16
+ "intent_type",
17
+ "correlation_id",
18
+ "payload",
19
+ }
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class DblPolicyAdapter(PolicyPort):
24
+ def decide(self, authoritative_input: Mapping[str, Any]) -> DecisionResult:
25
+ context = _build_policy_context(authoritative_input)
26
+ decision = _evaluate_policy(context)
27
+ gate_event = decision_to_dbl_event(decision, authoritative_input["correlation_id"])
28
+ policy_version = _policy_version_as_int(decision.policy_version.value)
29
+ return DecisionResult(
30
+ decision=decision.outcome.value,
31
+ reason_codes=[decision.reason_code],
32
+ policy_id=decision.policy_id.value,
33
+ policy_version=policy_version,
34
+ gate_event=gate_event,
35
+ )
36
+
37
+
38
+ def _build_policy_context(authoritative_input: Mapping[str, Any]) -> PolicyContext:
39
+ filtered = {key: authoritative_input.get(key) for key in ALLOWED_CONTEXT_KEYS}
40
+ tenant = authoritative_input.get("tenant_id", "unknown")
41
+ tenant_type = _tenant_id_type()
42
+ try:
43
+ tenant_value = tenant_type(str(tenant))
44
+ except Exception as exc:
45
+ raise RuntimeError("invalid tenant_id") from exc
46
+ return PolicyContext(tenant_id=tenant_value, inputs=filtered)
47
+
48
+
49
+ def _evaluate_policy(context: PolicyContext) -> PolicyDecision:
50
+ policy = _load_policy()
51
+ return policy.evaluate(context)
52
+
53
+
54
+ def _load_policy() -> Policy:
55
+ module_path = _get_env("DBL_GATEWAY_POLICY_MODULE")
56
+ obj_name = _get_env("DBL_GATEWAY_POLICY_OBJECT", "policy")
57
+ module = import_module(module_path)
58
+ obj = getattr(module, obj_name, None)
59
+ if obj is None:
60
+ raise RuntimeError("policy object not found")
61
+ if callable(obj) and not hasattr(obj, "evaluate"):
62
+ return obj() # type: ignore[return-value]
63
+ return obj # type: ignore[return-value]
64
+
65
+
66
+ def _get_env(name: str, default: str | None = None) -> str:
67
+ import os
68
+
69
+ value = os.getenv(name, "")
70
+ if value:
71
+ return value
72
+ if default is None:
73
+ raise RuntimeError(f"{name} is required")
74
+ return default
75
+
76
+
77
+ def _tenant_id_type() -> type:
78
+ hints = get_type_hints(PolicyContext)
79
+ tenant_type = hints.get("tenant_id")
80
+ if not isinstance(tenant_type, type):
81
+ raise RuntimeError("PolicyContext.tenant_id type missing")
82
+ return tenant_type
83
+
84
+
85
+ def _policy_version_as_int(value: object) -> int:
86
+ try:
87
+ if isinstance(value, str):
88
+ text = value.strip()
89
+ if text == "":
90
+ raise ValueError("empty")
91
+ if "." in text:
92
+ text = text.split(".", 1)[0]
93
+ return int(text)
94
+ return int(value)
95
+ except (TypeError, ValueError) as exc:
96
+ raise RuntimeError("policy_version must be int") from exc
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+ from ..ports.store_port import StorePort
7
+ from ..store.sqlite import SQLiteStore
8
+
9
+
10
+ @dataclass
11
+ class SQLiteStoreAdapter(StorePort):
12
+ _store: SQLiteStore
13
+
14
+ @classmethod
15
+ def from_path(cls, db_path: Path) -> "SQLiteStoreAdapter":
16
+ return cls(SQLiteStore(db_path))
17
+
18
+ def append(
19
+ self,
20
+ *,
21
+ kind: str,
22
+ lane: str,
23
+ actor: str,
24
+ intent_type: str,
25
+ stream_id: str,
26
+ correlation_id: str,
27
+ payload: dict[str, object],
28
+ ):
29
+ return self._store.append(
30
+ kind=kind,
31
+ lane=lane,
32
+ actor=actor,
33
+ intent_type=intent_type,
34
+ stream_id=stream_id,
35
+ correlation_id=correlation_id,
36
+ payload=payload,
37
+ )
38
+
39
+ def snapshot(
40
+ self,
41
+ *,
42
+ limit: int,
43
+ offset: int,
44
+ stream_id: str | None = None,
45
+ lane: str | None = None,
46
+ ):
47
+ return self._store.snapshot(
48
+ limit=limit,
49
+ offset=offset,
50
+ stream_id=stream_id,
51
+ lane=lane,
52
+ )
53
+
54
+ def close(self) -> None:
55
+ self._store.close()
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Mapping
5
+
6
+ from dbl_ingress import (
7
+ AdmissionError,
8
+ InvalidInputError,
9
+ AdmissionRecord,
10
+ shape_input,
11
+ ADMISSION_INVALID_INPUT,
12
+ ADMISSION_SECRETS_PRESENT,
13
+ )
14
+
15
+
16
+ SECRET_KEYS = {"api_key", "authorization", "token", "secret", "bearer"}
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class AdmissionFailure(Exception):
21
+ reason_code: str
22
+ detail: str
23
+
24
+
25
+ def admit_and_shape_intent(payload: Mapping[str, Any], *, raw_payload: Mapping[str, Any] | None = None) -> AdmissionRecord:
26
+ if raw_payload is not None and _contains_secrets(raw_payload):
27
+ raise AdmissionFailure(reason_code=ADMISSION_SECRETS_PRESENT, detail="secrets detected in payload")
28
+ if _contains_secrets(payload):
29
+ raise AdmissionFailure(reason_code=ADMISSION_SECRETS_PRESENT, detail="secrets detected in payload")
30
+
31
+ correlation_id = payload.get("correlation_id")
32
+ deterministic = payload.get("deterministic")
33
+ observational = payload.get("observational")
34
+
35
+ if not isinstance(correlation_id, str) or not correlation_id.strip():
36
+ raise AdmissionFailure(reason_code=ADMISSION_INVALID_INPUT, detail="correlation_id must be a non-empty string")
37
+ if not isinstance(deterministic, Mapping):
38
+ raise AdmissionFailure(reason_code=ADMISSION_INVALID_INPUT, detail="deterministic must be an object")
39
+ if observational is not None and not isinstance(observational, Mapping):
40
+ raise AdmissionFailure(reason_code=ADMISSION_INVALID_INPUT, detail="observational must be an object if provided")
41
+
42
+ try:
43
+ record = shape_input(
44
+ correlation_id=correlation_id,
45
+ deterministic=deterministic,
46
+ observational=observational,
47
+ )
48
+ except InvalidInputError as exc:
49
+ raise AdmissionFailure(reason_code=exc.reason_code, detail=str(exc)) from exc
50
+ except AdmissionError as exc:
51
+ reason = getattr(exc, "reason_code", ADMISSION_INVALID_INPUT)
52
+ raise AdmissionFailure(reason_code=reason, detail=str(exc)) from exc
53
+ return record
54
+
55
+
56
+ def _contains_secrets(value: object) -> bool:
57
+ if isinstance(value, Mapping):
58
+ for key, item in value.items():
59
+ if isinstance(key, str) and key.lower() in SECRET_KEYS:
60
+ if isinstance(item, str) and item.strip():
61
+ return True
62
+ if _contains_secrets(item):
63
+ return True
64
+ return False
65
+ if isinstance(value, list):
66
+ return any(_contains_secrets(item) for item in value)
67
+ return False