admina-framework 0.9.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.
- admina/__init__.py +34 -0
- admina/cli/__init__.py +14 -0
- admina/cli/commands/__init__.py +14 -0
- admina/cli/main.py +1522 -0
- admina/cli/templates/admina.yaml.j2 +77 -0
- admina/cli/templates/docker-compose.yml.j2 +254 -0
- admina/cli/templates/env.j2 +10 -0
- admina/cli/templates/main.py.j2 +95 -0
- admina/cli/templates/plugin.py.j2 +145 -0
- admina/cli/templates/plugin_pyproject.toml.j2 +15 -0
- admina/cli/templates/plugin_readme.md.j2 +27 -0
- admina/cli/templates/plugin_test.py.j2 +48 -0
- admina/core/__init__.py +14 -0
- admina/core/config.py +497 -0
- admina/core/event_bus.py +112 -0
- admina/core/secrets.py +257 -0
- admina/core/types.py +146 -0
- admina/dashboard/__init__.py +8 -0
- admina/dashboard/static/heimdall.png +0 -0
- admina/dashboard/static/index.html +1045 -0
- admina/dashboard/static/vendor/alpinejs.min.js +5 -0
- admina/domains/__init__.py +14 -0
- admina/domains/agent_security/__init__.py +41 -0
- admina/domains/agent_security/firewall.py +634 -0
- admina/domains/agent_security/loop_breaker.py +176 -0
- admina/domains/ai_infra/__init__.py +79 -0
- admina/domains/ai_infra/llm_engine.py +477 -0
- admina/domains/ai_infra/rag.py +817 -0
- admina/domains/ai_infra/webui.py +292 -0
- admina/domains/compliance/__init__.py +109 -0
- admina/domains/compliance/cross_regulation.py +314 -0
- admina/domains/compliance/eu_ai_act.py +367 -0
- admina/domains/compliance/forensic.py +380 -0
- admina/domains/compliance/gdpr.py +331 -0
- admina/domains/compliance/nis2.py +258 -0
- admina/domains/compliance/oisg.py +658 -0
- admina/domains/compliance/otel.py +101 -0
- admina/domains/data_sovereignty/__init__.py +42 -0
- admina/domains/data_sovereignty/classification.py +102 -0
- admina/domains/data_sovereignty/pii.py +260 -0
- admina/domains/data_sovereignty/residency.py +121 -0
- admina/integrations/__init__.py +14 -0
- admina/integrations/_engines.py +63 -0
- admina/integrations/cheshirecat/__init__.py +13 -0
- admina/integrations/cheshirecat/admina-plugin/admina_governance.py +207 -0
- admina/integrations/crewai/__init__.py +13 -0
- admina/integrations/crewai/callbacks.py +347 -0
- admina/integrations/langchain/__init__.py +13 -0
- admina/integrations/langchain/callbacks.py +341 -0
- admina/integrations/n8n/__init__.py +14 -0
- admina/integrations/openclaw/__init__.py +14 -0
- admina/plugins/__init__.py +49 -0
- admina/plugins/base.py +633 -0
- admina/plugins/builtin/__init__.py +14 -0
- admina/plugins/builtin/adapters/__init__.py +14 -0
- admina/plugins/builtin/adapters/ollama.py +120 -0
- admina/plugins/builtin/adapters/openai.py +138 -0
- admina/plugins/builtin/alerts/__init__.py +14 -0
- admina/plugins/builtin/alerts/log.py +66 -0
- admina/plugins/builtin/alerts/webhook.py +102 -0
- admina/plugins/builtin/auth/__init__.py +14 -0
- admina/plugins/builtin/auth/apikey.py +138 -0
- admina/plugins/builtin/compliance/__init__.py +14 -0
- admina/plugins/builtin/compliance/eu_ai_act.py +202 -0
- admina/plugins/builtin/connectors/__init__.py +14 -0
- admina/plugins/builtin/connectors/chromadb.py +137 -0
- admina/plugins/builtin/connectors/filesystem.py +111 -0
- admina/plugins/builtin/forensic/__init__.py +14 -0
- admina/plugins/builtin/forensic/filesystem.py +163 -0
- admina/plugins/builtin/forensic/minio.py +180 -0
- admina/plugins/builtin/guards/__init__.py +0 -0
- admina/plugins/builtin/guards/guardrailsai_guard.py +172 -0
- admina/plugins/builtin/pii/__init__.py +14 -0
- admina/plugins/builtin/pii/spacy_regex.py +160 -0
- admina/plugins/builtin/transports/__init__.py +14 -0
- admina/plugins/builtin/transports/http_rest.py +97 -0
- admina/plugins/builtin/transports/mcp.py +173 -0
- admina/plugins/registry.py +356 -0
- admina/proxy/__init__.py +15 -0
- admina/proxy/api/__init__.py +17 -0
- admina/proxy/api/dashboard.py +925 -0
- admina/proxy/api/integration.py +153 -0
- admina/proxy/config.py +214 -0
- admina/proxy/engine_bridge.py +306 -0
- admina/proxy/governance.py +232 -0
- admina/proxy/main.py +1484 -0
- admina/proxy/multi_upstream.py +156 -0
- admina/proxy/state.py +97 -0
- admina/py.typed +0 -0
- admina/sdk/__init__.py +34 -0
- admina/sdk/_compat.py +43 -0
- admina/sdk/compliance_kit.py +359 -0
- admina/sdk/governed_agent.py +391 -0
- admina/sdk/governed_data.py +434 -0
- admina/sdk/governed_model.py +241 -0
- admina_framework-0.9.0.dist-info/METADATA +575 -0
- admina_framework-0.9.0.dist-info/RECORD +102 -0
- admina_framework-0.9.0.dist-info/WHEEL +5 -0
- admina_framework-0.9.0.dist-info/entry_points.txt +2 -0
- admina_framework-0.9.0.dist-info/licenses/LICENSE +191 -0
- admina_framework-0.9.0.dist-info/licenses/NOTICE +16 -0
- admina_framework-0.9.0.dist-info/top_level.txt +1 -0
admina/core/secrets.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Copyright © 2025–2026 Stefano Noferi & Admina contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Admina — Secret Vault
|
|
16
|
+
|
|
17
|
+
Generates, stores, and retrieves secrets for the Admina platform.
|
|
18
|
+
Secrets are encrypted at rest using Fernet symmetric encryption.
|
|
19
|
+
|
|
20
|
+
Vault layout (per-project, under ``<project>/.admina/``):
|
|
21
|
+
|
|
22
|
+
.admina/
|
|
23
|
+
vault.key — Fernet key (mode 0600, never committed)
|
|
24
|
+
secrets.json — encrypted secret values
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import re
|
|
32
|
+
import secrets
|
|
33
|
+
import string
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
|
|
36
|
+
from cryptography.fernet import Fernet
|
|
37
|
+
|
|
38
|
+
# ── Secret Generation ────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def generate_api_key() -> str:
|
|
42
|
+
"""Generate a 64-char hex API key (256-bit entropy)."""
|
|
43
|
+
return secrets.token_hex(32)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate_password(length: int = 20) -> str:
|
|
47
|
+
"""Generate a random password meeting quality requirements.
|
|
48
|
+
|
|
49
|
+
Guarantees at least one uppercase, one lowercase, one digit,
|
|
50
|
+
and one special character.
|
|
51
|
+
"""
|
|
52
|
+
if length < 12:
|
|
53
|
+
length = 12
|
|
54
|
+
|
|
55
|
+
upper = string.ascii_uppercase
|
|
56
|
+
lower = string.ascii_lowercase
|
|
57
|
+
digits = string.digits
|
|
58
|
+
special = "!@#%&*+-"
|
|
59
|
+
|
|
60
|
+
# Guarantee one of each class
|
|
61
|
+
password_chars = [
|
|
62
|
+
secrets.choice(upper),
|
|
63
|
+
secrets.choice(lower),
|
|
64
|
+
secrets.choice(digits),
|
|
65
|
+
secrets.choice(special),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
# Fill the rest from the full alphabet
|
|
69
|
+
alphabet = upper + lower + digits + special
|
|
70
|
+
for _ in range(length - len(password_chars)):
|
|
71
|
+
password_chars.append(secrets.choice(alphabet))
|
|
72
|
+
|
|
73
|
+
# Shuffle to avoid predictable positions
|
|
74
|
+
shuffled = list(password_chars)
|
|
75
|
+
for i in range(len(shuffled) - 1, 0, -1):
|
|
76
|
+
j = secrets.randbelow(i + 1)
|
|
77
|
+
shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
|
|
78
|
+
|
|
79
|
+
return "".join(shuffled)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Password Validation ──────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
_MIN_PASSWORD_LENGTH = 12
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def validate_password(password: str) -> tuple[bool, list[str]]:
|
|
88
|
+
"""Check password quality. Returns (ok, list_of_issues)."""
|
|
89
|
+
issues: list[str] = []
|
|
90
|
+
|
|
91
|
+
if len(password) < _MIN_PASSWORD_LENGTH:
|
|
92
|
+
issues.append(f"Minimum {_MIN_PASSWORD_LENGTH} characters (got {len(password)})")
|
|
93
|
+
if not re.search(r"[A-Z]", password):
|
|
94
|
+
issues.append("At least one uppercase letter")
|
|
95
|
+
if not re.search(r"[a-z]", password):
|
|
96
|
+
issues.append("At least one lowercase letter")
|
|
97
|
+
if not re.search(r"\d", password):
|
|
98
|
+
issues.append("At least one digit")
|
|
99
|
+
if not re.search(r"[^A-Za-z0-9]", password):
|
|
100
|
+
issues.append("At least one special character")
|
|
101
|
+
|
|
102
|
+
return (len(issues) == 0, issues)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ── Secret Vault ─────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
# Keys managed by the vault
|
|
108
|
+
VAULT_KEYS = (
|
|
109
|
+
"ADMINA_API_KEY",
|
|
110
|
+
"ADMINA_DASHBOARD_PASSWORD",
|
|
111
|
+
"CLICKHOUSE_PASSWORD",
|
|
112
|
+
"GRAFANA_ADMIN_PASSWORD",
|
|
113
|
+
"MINIO_SECRET_KEY",
|
|
114
|
+
# Used by the optional Open WebUI service in docker-compose when
|
|
115
|
+
# ai_infra.webui is enabled. Generated unconditionally so the
|
|
116
|
+
# compose file works whether or not the operator picked the
|
|
117
|
+
# ai_infra domain — it's just a random session secret.
|
|
118
|
+
"WEBUI_SECRET_KEY",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SecretVault:
|
|
123
|
+
"""Encrypted secret store backed by a local JSON file.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
project_dir:
|
|
128
|
+
Root of the Admina project. The vault lives at
|
|
129
|
+
``<project_dir>/.admina/secrets.json``.
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(self, project_dir: str | Path) -> None:
|
|
133
|
+
self._dir = Path(project_dir) / ".admina"
|
|
134
|
+
self._key_path = self._dir / "vault.key"
|
|
135
|
+
self._secrets_path = self._dir / "secrets.json"
|
|
136
|
+
self._fernet: Fernet | None = None
|
|
137
|
+
|
|
138
|
+
# ── public API ────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def is_initialized(self) -> bool:
|
|
142
|
+
"""True if the vault has been bootstrapped at least once."""
|
|
143
|
+
return self._secrets_path.is_file() and self._key_path.is_file()
|
|
144
|
+
|
|
145
|
+
def bootstrap(self) -> dict[str, str]:
|
|
146
|
+
"""Generate all platform secrets and store them in the vault.
|
|
147
|
+
|
|
148
|
+
Returns the generated secrets as a plain dict (for one-time display).
|
|
149
|
+
"""
|
|
150
|
+
generated: dict[str, str] = {
|
|
151
|
+
"ADMINA_API_KEY": generate_api_key(),
|
|
152
|
+
"ADMINA_DASHBOARD_PASSWORD": generate_password(),
|
|
153
|
+
"CLICKHOUSE_PASSWORD": generate_password(),
|
|
154
|
+
"GRAFANA_ADMIN_PASSWORD": generate_password(),
|
|
155
|
+
"MINIO_SECRET_KEY": generate_password(),
|
|
156
|
+
# Independent random secret for the optional Open WebUI
|
|
157
|
+
# session cookie — never share it with the user-facing
|
|
158
|
+
# dashboard password.
|
|
159
|
+
"WEBUI_SECRET_KEY": generate_api_key(),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# Use the same dashboard password for all web UIs (but NOT
|
|
163
|
+
# the WEBUI session secret, which is internal only).
|
|
164
|
+
shared_password = generated["ADMINA_DASHBOARD_PASSWORD"]
|
|
165
|
+
generated["CLICKHOUSE_PASSWORD"] = shared_password
|
|
166
|
+
generated["GRAFANA_ADMIN_PASSWORD"] = shared_password
|
|
167
|
+
generated["MINIO_SECRET_KEY"] = shared_password
|
|
168
|
+
|
|
169
|
+
self._ensure_dir()
|
|
170
|
+
self._ensure_key()
|
|
171
|
+
self._save(generated)
|
|
172
|
+
return generated
|
|
173
|
+
|
|
174
|
+
def get(self, key: str) -> str | None:
|
|
175
|
+
"""Retrieve a single secret by key. Returns None if not found."""
|
|
176
|
+
data = self._load()
|
|
177
|
+
return data.get(key)
|
|
178
|
+
|
|
179
|
+
def get_all(self) -> dict[str, str]:
|
|
180
|
+
"""Return all secrets as a plain dict."""
|
|
181
|
+
return self._load()
|
|
182
|
+
|
|
183
|
+
def set(self, key: str, value: str) -> None:
|
|
184
|
+
"""Set or update a single secret."""
|
|
185
|
+
data = self._load()
|
|
186
|
+
data[key] = value
|
|
187
|
+
self._save(data)
|
|
188
|
+
|
|
189
|
+
def update_password(self, new_password: str) -> None:
|
|
190
|
+
"""Update the shared web UI password across all services."""
|
|
191
|
+
data = self._load()
|
|
192
|
+
data["ADMINA_DASHBOARD_PASSWORD"] = new_password
|
|
193
|
+
data["CLICKHOUSE_PASSWORD"] = new_password
|
|
194
|
+
data["GRAFANA_ADMIN_PASSWORD"] = new_password
|
|
195
|
+
data["MINIO_SECRET_KEY"] = new_password
|
|
196
|
+
self._save(data)
|
|
197
|
+
|
|
198
|
+
def export_env(self) -> dict[str, str]:
|
|
199
|
+
"""Return secrets suitable for injection into subprocess env.
|
|
200
|
+
|
|
201
|
+
Same as get_all() but filters to known VAULT_KEYS only.
|
|
202
|
+
"""
|
|
203
|
+
data = self._load()
|
|
204
|
+
return {k: v for k, v in data.items() if k in VAULT_KEYS}
|
|
205
|
+
|
|
206
|
+
def write_dotenv(self, env_path: str | Path) -> None:
|
|
207
|
+
"""Write a .env file with the current vault secrets.
|
|
208
|
+
|
|
209
|
+
The file is written with mode 0600.
|
|
210
|
+
"""
|
|
211
|
+
data = self.export_env()
|
|
212
|
+
lines = [
|
|
213
|
+
"# Auto-generated by admina vault. Do not edit manually.",
|
|
214
|
+
"# Regenerate with: admina password reset",
|
|
215
|
+
"",
|
|
216
|
+
]
|
|
217
|
+
for key in VAULT_KEYS:
|
|
218
|
+
value = data.get(key, "")
|
|
219
|
+
# Use double quotes — Docker Compose env_file strips them correctly.
|
|
220
|
+
# Single quotes are taken literally by some env_file parsers.
|
|
221
|
+
lines.append(f'{key}="{value}"')
|
|
222
|
+
lines.append("")
|
|
223
|
+
|
|
224
|
+
env_path = Path(env_path)
|
|
225
|
+
env_path.write_text("\n".join(lines))
|
|
226
|
+
os.chmod(env_path, 0o600)
|
|
227
|
+
|
|
228
|
+
# ── internal ──────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
def _ensure_dir(self) -> None:
|
|
231
|
+
self._dir.mkdir(parents=True, exist_ok=True)
|
|
232
|
+
|
|
233
|
+
def _ensure_key(self) -> None:
|
|
234
|
+
if not self._key_path.is_file():
|
|
235
|
+
key = Fernet.generate_key()
|
|
236
|
+
self._key_path.write_bytes(key)
|
|
237
|
+
os.chmod(self._key_path, 0o600)
|
|
238
|
+
self._fernet = None # reset cached fernet
|
|
239
|
+
|
|
240
|
+
def _get_fernet(self) -> Fernet:
|
|
241
|
+
if self._fernet is None:
|
|
242
|
+
key = self._key_path.read_bytes().strip()
|
|
243
|
+
self._fernet = Fernet(key)
|
|
244
|
+
return self._fernet
|
|
245
|
+
|
|
246
|
+
def _save(self, data: dict[str, str]) -> None:
|
|
247
|
+
plaintext = json.dumps(data, indent=2).encode()
|
|
248
|
+
encrypted = self._get_fernet().encrypt(plaintext)
|
|
249
|
+
self._secrets_path.write_bytes(encrypted)
|
|
250
|
+
os.chmod(self._secrets_path, 0o600)
|
|
251
|
+
|
|
252
|
+
def _load(self) -> dict[str, str]:
|
|
253
|
+
if not self._secrets_path.is_file():
|
|
254
|
+
return {}
|
|
255
|
+
encrypted = self._secrets_path.read_bytes()
|
|
256
|
+
plaintext = self._get_fernet().decrypt(encrypted)
|
|
257
|
+
return json.loads(plaintext)
|
admina/core/types.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Copyright © 2025–2026 Stefano Noferi & Admina contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Admina — Protocol-agnostic governance types.
|
|
16
|
+
|
|
17
|
+
These dataclasses decouple the governance engine from any wire format.
|
|
18
|
+
Transport adapters (MCP, A2A, AG-UI, REST) convert to/from these types.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import time
|
|
24
|
+
import uuid
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from enum import Enum
|
|
27
|
+
from typing import Any, Literal
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"RiskLevel",
|
|
31
|
+
"GovernanceAction",
|
|
32
|
+
"EventType",
|
|
33
|
+
"GovernanceRequest",
|
|
34
|
+
"GovernanceResponse",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RiskLevel(str, Enum):
|
|
39
|
+
"""Risk severity levels used across all governance domains.
|
|
40
|
+
|
|
41
|
+
Using ``str`` mixin so values serialise directly to JSON strings.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
LOW = "low"
|
|
45
|
+
MEDIUM = "medium"
|
|
46
|
+
HIGH = "high"
|
|
47
|
+
CRITICAL = "critical"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GovernanceAction(str, Enum):
|
|
51
|
+
"""Actions the governance engine can take on a request."""
|
|
52
|
+
|
|
53
|
+
ALLOW = "allow"
|
|
54
|
+
BLOCK = "block"
|
|
55
|
+
REDACT = "redact"
|
|
56
|
+
ESCALATE = "escalate"
|
|
57
|
+
CIRCUIT_BREAK = "circuit_break"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EventType(str, Enum):
|
|
61
|
+
"""Unified event types used across proxy and SDK."""
|
|
62
|
+
|
|
63
|
+
# Proxy / MCP events
|
|
64
|
+
MCP_REQUEST = "mcp_request"
|
|
65
|
+
MCP_RESPONSE = "mcp_response"
|
|
66
|
+
INJECTION_DETECTED = "injection_detected"
|
|
67
|
+
PII_REDACTED = "pii_redacted"
|
|
68
|
+
LOOP_DETECTED = "loop_detected"
|
|
69
|
+
POLICY_VIOLATION = "policy_violation"
|
|
70
|
+
CIRCUIT_BREAK = "circuit_break"
|
|
71
|
+
|
|
72
|
+
# SDK / framework events
|
|
73
|
+
MODEL_CALL = "model.call"
|
|
74
|
+
MODEL_RESPONSE = "model.response"
|
|
75
|
+
DATA_ACCESS = "data.access"
|
|
76
|
+
DATA_CLASSIFY = "data.classify"
|
|
77
|
+
DATA_REDACT = "data.redact"
|
|
78
|
+
AGENT_REQUEST = "agent.request"
|
|
79
|
+
AGENT_RESPONSE = "agent.response"
|
|
80
|
+
GOVERNANCE_DECISION = "governance.decision"
|
|
81
|
+
COMPLIANCE_CHECK = "compliance.check"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class GovernanceRequest:
|
|
86
|
+
"""A protocol-agnostic inbound request to the governance engine.
|
|
87
|
+
|
|
88
|
+
Transport adapters normalize protocol-specific messages into this
|
|
89
|
+
dataclass before the governance pipeline processes them.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
content: str
|
|
93
|
+
method: str = ""
|
|
94
|
+
direction: Literal["inbound", "outbound"] = "inbound"
|
|
95
|
+
session_id: str | None = None
|
|
96
|
+
user_id: str | None = None
|
|
97
|
+
agent_id: str | None = None
|
|
98
|
+
protocol: str = "unknown"
|
|
99
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
100
|
+
raw: Any = None
|
|
101
|
+
request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
|
|
102
|
+
timestamp_us: float = field(default_factory=lambda: time.time() * 1_000_000)
|
|
103
|
+
|
|
104
|
+
def to_dict(self) -> dict[str, Any]:
|
|
105
|
+
"""Serialize to a plain dict (excluding ``raw``)."""
|
|
106
|
+
return {
|
|
107
|
+
"request_id": self.request_id,
|
|
108
|
+
"content": self.content,
|
|
109
|
+
"method": self.method,
|
|
110
|
+
"direction": self.direction,
|
|
111
|
+
"session_id": self.session_id,
|
|
112
|
+
"user_id": self.user_id,
|
|
113
|
+
"agent_id": self.agent_id,
|
|
114
|
+
"protocol": self.protocol,
|
|
115
|
+
"metadata": self.metadata,
|
|
116
|
+
"timestamp_us": self.timestamp_us,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class GovernanceResponse:
|
|
122
|
+
"""The governance engine's decision for a single request.
|
|
123
|
+
|
|
124
|
+
Returned by the governance pipeline and converted back to wire format
|
|
125
|
+
by the transport adapter.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
content: str
|
|
129
|
+
action: Literal["ALLOW", "BLOCK", "REDACT", "CIRCUIT_BREAK"] = "ALLOW"
|
|
130
|
+
risk_level: Literal["LOW", "MEDIUM", "HIGH", "CRITICAL"] = "LOW"
|
|
131
|
+
domain: str = ""
|
|
132
|
+
latency_us: float = 0.0
|
|
133
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
134
|
+
request_id: str = ""
|
|
135
|
+
|
|
136
|
+
def to_dict(self) -> dict[str, Any]:
|
|
137
|
+
"""Serialize to a plain dict."""
|
|
138
|
+
return {
|
|
139
|
+
"request_id": self.request_id,
|
|
140
|
+
"content": self.content,
|
|
141
|
+
"action": self.action,
|
|
142
|
+
"risk_level": self.risk_level,
|
|
143
|
+
"domain": self.domain,
|
|
144
|
+
"latency_us": self.latency_us,
|
|
145
|
+
"metadata": self.metadata,
|
|
146
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Copyright © 2025–2026 Stefano Noferi & Admina contributors
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
"""Admina built-in dashboard.
|
|
4
|
+
|
|
5
|
+
Static SPA (Alpine.js) served by FastAPI when running `admina dev`
|
|
6
|
+
locally (no Docker). The same files are also served by nginx in the
|
|
7
|
+
Docker stack — see ../../dashboard/Dockerfile.
|
|
8
|
+
"""
|
|
Binary file
|