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
|
@@ -0,0 +1,391 @@
|
|
|
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 — GovernedAgent SDK primitive.
|
|
16
|
+
|
|
17
|
+
Exposes the existing proxy governance pipeline (firewall, loop breaker,
|
|
18
|
+
PII redaction) as a Python object for programmatic agent-to-agent calls.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import time
|
|
24
|
+
import uuid
|
|
25
|
+
from collections.abc import Awaitable, Callable
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from admina.core.event_bus import EventType, GovernanceEvent, bus
|
|
30
|
+
from admina.sdk._compat import run_sync
|
|
31
|
+
|
|
32
|
+
__all__ = ["GovernedAgent", "GovernedMCPResponse"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class GovernedMCPResponse:
|
|
37
|
+
"""Response from GovernedAgent.call().
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
result: The upstream response payload.
|
|
41
|
+
action: Governance action taken (ALLOW, BLOCK, REDACT, CIRCUIT_BREAK).
|
|
42
|
+
risk_level: Risk level of the request.
|
|
43
|
+
governance: Full governance details (domain results, latency).
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
result: Any = None
|
|
47
|
+
action: str = "ALLOW"
|
|
48
|
+
risk_level: str = "LOW"
|
|
49
|
+
governance: dict[str, Any] = field(default_factory=dict)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
UpstreamCallable = Callable[..., Awaitable[dict]]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load_firewall() -> Any:
|
|
56
|
+
"""Lazily load the InjectionFirewall."""
|
|
57
|
+
from admina.domains.agent_security.firewall import InjectionFirewall
|
|
58
|
+
|
|
59
|
+
return InjectionFirewall()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _load_loop_breaker() -> Any:
|
|
63
|
+
"""Lazily load the LoopBreaker."""
|
|
64
|
+
from admina.domains.agent_security.loop_breaker import LoopBreaker
|
|
65
|
+
|
|
66
|
+
return LoopBreaker()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _load_pii_redactor() -> Any:
|
|
70
|
+
"""Lazily load the PII redactor."""
|
|
71
|
+
from admina.domains.data_sovereignty.pii import PIIRedactor
|
|
72
|
+
|
|
73
|
+
return PIIRedactor()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _extract_text(params: dict) -> str:
|
|
77
|
+
"""Extract scannable text from params dict.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
params: The call params.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Concatenated string values for scanning.
|
|
84
|
+
"""
|
|
85
|
+
texts: list[str] = []
|
|
86
|
+
_collect_strings(params, texts, depth=0)
|
|
87
|
+
return " ".join(texts) if texts else ""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _collect_strings(obj: Any, acc: list[str], depth: int) -> None:
|
|
91
|
+
"""Recursively collect string values from nested structures.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
obj: Value to inspect.
|
|
95
|
+
acc: Accumulator list.
|
|
96
|
+
depth: Recursion depth (capped at 5).
|
|
97
|
+
"""
|
|
98
|
+
if depth > 5:
|
|
99
|
+
return
|
|
100
|
+
if isinstance(obj, str):
|
|
101
|
+
acc.append(obj)
|
|
102
|
+
elif isinstance(obj, dict):
|
|
103
|
+
for v in obj.values():
|
|
104
|
+
_collect_strings(v, acc, depth + 1)
|
|
105
|
+
elif isinstance(obj, list):
|
|
106
|
+
for item in obj:
|
|
107
|
+
_collect_strings(item, acc, depth + 1)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _redact_dict(obj: Any, pii_redactor: Any, depth: int = 0) -> tuple[Any, int]:
|
|
111
|
+
"""Recursively redact PII in nested structures.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
obj: Value to redact.
|
|
115
|
+
pii_redactor: Object with .redact(text) method.
|
|
116
|
+
depth: Recursion depth (capped at 5).
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Tuple of (redacted value, total PII count).
|
|
120
|
+
"""
|
|
121
|
+
if depth > 5:
|
|
122
|
+
return obj, 0
|
|
123
|
+
if isinstance(obj, str):
|
|
124
|
+
r = pii_redactor.redact(obj)
|
|
125
|
+
return r["redacted_text"], r["count"]
|
|
126
|
+
if isinstance(obj, dict):
|
|
127
|
+
total = 0
|
|
128
|
+
out = {}
|
|
129
|
+
for k, v in obj.items():
|
|
130
|
+
rv, c = _redact_dict(v, pii_redactor, depth + 1)
|
|
131
|
+
out[k] = rv
|
|
132
|
+
total += c
|
|
133
|
+
return out, total
|
|
134
|
+
if isinstance(obj, list):
|
|
135
|
+
total = 0
|
|
136
|
+
out_list = []
|
|
137
|
+
for item in obj:
|
|
138
|
+
rv, c = _redact_dict(item, pii_redactor, depth + 1)
|
|
139
|
+
out_list.append(rv)
|
|
140
|
+
total += c
|
|
141
|
+
return out_list, total
|
|
142
|
+
return obj, 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class GovernedAgent:
|
|
146
|
+
"""SDK primitive for governed agent-to-agent calls.
|
|
147
|
+
|
|
148
|
+
Wraps an upstream callable with the full governance pipeline:
|
|
149
|
+
firewall check, loop detection, PII redaction (bidirectional),
|
|
150
|
+
and event emission.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
upstream: An async callable that performs the actual upstream call,
|
|
154
|
+
accepting (method, params, **kwargs) and returning a dict.
|
|
155
|
+
audit: Whether to emit governance events.
|
|
156
|
+
pii_redaction: Whether to run PII redaction.
|
|
157
|
+
firewall_enabled: Whether to run the injection firewall.
|
|
158
|
+
loop_detection: Whether to run loop detection.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
def __init__(
|
|
162
|
+
self,
|
|
163
|
+
upstream: UpstreamCallable,
|
|
164
|
+
audit: bool = True,
|
|
165
|
+
pii_redaction: bool = True,
|
|
166
|
+
firewall_enabled: bool = True,
|
|
167
|
+
loop_detection: bool = True,
|
|
168
|
+
) -> None:
|
|
169
|
+
"""Initialize GovernedAgent.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
upstream: Async callable for upstream calls.
|
|
173
|
+
audit: If True, emit events to the event bus.
|
|
174
|
+
pii_redaction: If True, redact PII bidirectionally.
|
|
175
|
+
firewall_enabled: If True, run injection firewall.
|
|
176
|
+
loop_detection: If True, run loop breaker.
|
|
177
|
+
"""
|
|
178
|
+
self._upstream = upstream
|
|
179
|
+
self._audit = audit
|
|
180
|
+
self._pii_redaction = pii_redaction
|
|
181
|
+
self._firewall_enabled = firewall_enabled
|
|
182
|
+
self._loop_detection = loop_detection
|
|
183
|
+
self._firewall: Any = None
|
|
184
|
+
self._loop_breaker: Any = None
|
|
185
|
+
self._pii_redactor: Any = None
|
|
186
|
+
|
|
187
|
+
def _get_firewall(self) -> Any:
|
|
188
|
+
"""Return the firewall, creating it lazily."""
|
|
189
|
+
if self._firewall is None:
|
|
190
|
+
self._firewall = _load_firewall()
|
|
191
|
+
return self._firewall
|
|
192
|
+
|
|
193
|
+
def _get_loop_breaker(self) -> Any:
|
|
194
|
+
"""Return the loop breaker, creating it lazily."""
|
|
195
|
+
if self._loop_breaker is None:
|
|
196
|
+
self._loop_breaker = _load_loop_breaker()
|
|
197
|
+
return self._loop_breaker
|
|
198
|
+
|
|
199
|
+
def _get_pii_redactor(self) -> Any:
|
|
200
|
+
"""Return the PII redactor, creating it lazily."""
|
|
201
|
+
if self._pii_redactor is None:
|
|
202
|
+
self._pii_redactor = _load_pii_redactor()
|
|
203
|
+
return self._pii_redactor
|
|
204
|
+
|
|
205
|
+
async def call(
|
|
206
|
+
self,
|
|
207
|
+
method: str,
|
|
208
|
+
params: dict,
|
|
209
|
+
**kwargs: Any,
|
|
210
|
+
) -> GovernedMCPResponse:
|
|
211
|
+
"""Make a governed upstream call.
|
|
212
|
+
|
|
213
|
+
Runs the governance pipeline on the request, forwards to
|
|
214
|
+
upstream if allowed, then inspects the response bidirectionally.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
method: The RPC method name.
|
|
218
|
+
params: The call parameters.
|
|
219
|
+
**kwargs: Additional arguments (session_id, etc.).
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
GovernedMCPResponse with result, action, and governance info.
|
|
223
|
+
"""
|
|
224
|
+
session_id = kwargs.pop("session_id", None) or str(uuid.uuid4())
|
|
225
|
+
start_us = time.time() * 1_000_000
|
|
226
|
+
governance: dict[str, Any] = {}
|
|
227
|
+
|
|
228
|
+
# 1. Emit AGENT_REQUEST event
|
|
229
|
+
if self._audit:
|
|
230
|
+
await bus.emit(
|
|
231
|
+
GovernanceEvent(
|
|
232
|
+
event_type=EventType.AGENT_REQUEST,
|
|
233
|
+
session_id=session_id,
|
|
234
|
+
domain="agent-security",
|
|
235
|
+
metadata={"method": method},
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
text_to_scan = _extract_text(params)
|
|
240
|
+
|
|
241
|
+
# 2. Firewall check
|
|
242
|
+
if self._firewall_enabled and text_to_scan:
|
|
243
|
+
fw_result = self._get_firewall().check(text_to_scan)
|
|
244
|
+
governance["firewall"] = fw_result
|
|
245
|
+
if fw_result.get("is_injection"):
|
|
246
|
+
return await self._blocked_response(
|
|
247
|
+
session_id,
|
|
248
|
+
start_us,
|
|
249
|
+
"BLOCK",
|
|
250
|
+
fw_result.get("risk_level", "HIGH"),
|
|
251
|
+
"firewall",
|
|
252
|
+
governance,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# 3. Loop detection
|
|
256
|
+
if self._loop_detection and text_to_scan:
|
|
257
|
+
loop_result = self._get_loop_breaker().check(
|
|
258
|
+
session_id,
|
|
259
|
+
text_to_scan,
|
|
260
|
+
)
|
|
261
|
+
governance["loop_breaker"] = loop_result
|
|
262
|
+
if loop_result.get("is_loop"):
|
|
263
|
+
return await self._blocked_response(
|
|
264
|
+
session_id,
|
|
265
|
+
start_us,
|
|
266
|
+
"CIRCUIT_BREAK",
|
|
267
|
+
"HIGH",
|
|
268
|
+
"loop_breaker",
|
|
269
|
+
governance,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# 4. PII redaction on request (inbound)
|
|
273
|
+
redacted_params = params
|
|
274
|
+
if self._pii_redaction:
|
|
275
|
+
redacted_params, pii_count = _redact_dict(
|
|
276
|
+
params,
|
|
277
|
+
self._get_pii_redactor(),
|
|
278
|
+
)
|
|
279
|
+
governance["pii_request"] = {
|
|
280
|
+
"redacted": pii_count > 0,
|
|
281
|
+
"count": pii_count,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# 5. Forward to upstream
|
|
285
|
+
upstream_result = await self._upstream(method, redacted_params, **kwargs)
|
|
286
|
+
|
|
287
|
+
# 6. PII redaction on response (outbound)
|
|
288
|
+
redacted_result = upstream_result
|
|
289
|
+
if self._pii_redaction and isinstance(upstream_result, dict):
|
|
290
|
+
redacted_result, pii_count = _redact_dict(
|
|
291
|
+
upstream_result,
|
|
292
|
+
self._get_pii_redactor(),
|
|
293
|
+
)
|
|
294
|
+
governance["pii_response"] = {
|
|
295
|
+
"redacted": pii_count > 0,
|
|
296
|
+
"count": pii_count,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
latency_us = time.time() * 1_000_000 - start_us
|
|
300
|
+
governance["latency_us"] = latency_us
|
|
301
|
+
|
|
302
|
+
action = "ALLOW"
|
|
303
|
+
pii_req = governance.get("pii_request", {}).get("count", 0)
|
|
304
|
+
pii_resp = governance.get("pii_response", {}).get("count", 0)
|
|
305
|
+
if pii_req > 0 or pii_resp > 0:
|
|
306
|
+
action = "REDACT"
|
|
307
|
+
|
|
308
|
+
# 7. Emit AGENT_RESPONSE event
|
|
309
|
+
if self._audit:
|
|
310
|
+
await bus.emit(
|
|
311
|
+
GovernanceEvent(
|
|
312
|
+
event_type=EventType.AGENT_RESPONSE,
|
|
313
|
+
session_id=session_id,
|
|
314
|
+
domain="agent-security",
|
|
315
|
+
action=action,
|
|
316
|
+
metadata={
|
|
317
|
+
"method": method,
|
|
318
|
+
"latency_us": latency_us,
|
|
319
|
+
},
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
return GovernedMCPResponse(
|
|
324
|
+
result=redacted_result,
|
|
325
|
+
action=action,
|
|
326
|
+
risk_level="LOW",
|
|
327
|
+
governance=governance,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
async def _blocked_response(
|
|
331
|
+
self,
|
|
332
|
+
session_id: str,
|
|
333
|
+
start_us: float,
|
|
334
|
+
action: str,
|
|
335
|
+
risk_level: Any,
|
|
336
|
+
domain: str,
|
|
337
|
+
governance: dict[str, Any],
|
|
338
|
+
) -> GovernedMCPResponse:
|
|
339
|
+
"""Build a blocked response and emit the event.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
session_id: Session identifier.
|
|
343
|
+
start_us: Start time in microseconds.
|
|
344
|
+
action: The governance action (BLOCK, CIRCUIT_BREAK).
|
|
345
|
+
risk_level: The risk level.
|
|
346
|
+
domain: Which domain blocked the request.
|
|
347
|
+
governance: Governance dict to include.
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
GovernedMCPResponse with the block decision.
|
|
351
|
+
"""
|
|
352
|
+
latency_us = time.time() * 1_000_000 - start_us
|
|
353
|
+
governance["latency_us"] = latency_us
|
|
354
|
+
|
|
355
|
+
risk_str = str(risk_level)
|
|
356
|
+
if hasattr(risk_level, "value"):
|
|
357
|
+
risk_str = str(risk_level.value).upper()
|
|
358
|
+
else:
|
|
359
|
+
risk_str = risk_str.upper()
|
|
360
|
+
|
|
361
|
+
if self._audit:
|
|
362
|
+
await bus.emit(
|
|
363
|
+
GovernanceEvent(
|
|
364
|
+
event_type=EventType.AGENT_RESPONSE,
|
|
365
|
+
session_id=session_id,
|
|
366
|
+
domain="agent-security",
|
|
367
|
+
action=action,
|
|
368
|
+
risk_level=risk_str,
|
|
369
|
+
metadata={"domain": domain},
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return GovernedMCPResponse(
|
|
374
|
+
result=None,
|
|
375
|
+
action=action,
|
|
376
|
+
risk_level=risk_str,
|
|
377
|
+
governance=governance,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def call_sync(self, method: str, params: dict, **kwargs: Any) -> GovernedMCPResponse:
|
|
381
|
+
"""Synchronous convenience wrapper around call().
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
method: The RPC method name.
|
|
385
|
+
params: The call parameters.
|
|
386
|
+
**kwargs: Forwarded to call().
|
|
387
|
+
|
|
388
|
+
Returns:
|
|
389
|
+
GovernedMCPResponse.
|
|
390
|
+
"""
|
|
391
|
+
return run_sync(self.call(method, params, **kwargs))
|