sernixa 0.1.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.
- sernixa/__init__.py +94 -0
- sernixa/adapters/__init__.py +17 -0
- sernixa/adapters/_common.py +56 -0
- sernixa/adapters/autogen.py +32 -0
- sernixa/adapters/crewai.py +133 -0
- sernixa/adapters/langchain.py +146 -0
- sernixa/adapters/mcp.py +179 -0
- sernixa/client.py +1015 -0
- sernixa/dlp.py +90 -0
- sernixa/exceptions.py +55 -0
- sernixa/langchain_adapter.py +30 -0
- sernixa-0.1.0.dist-info/METADATA +378 -0
- sernixa-0.1.0.dist-info/RECORD +15 -0
- sernixa-0.1.0.dist-info/WHEEL +5 -0
- sernixa-0.1.0.dist-info/top_level.txt +1 -0
sernixa/__init__.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import importlib.metadata as _im
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
__version__ = _im.version("sernixa")
|
|
5
|
+
except _im.PackageNotFoundError:
|
|
6
|
+
__version__ = "unknown"
|
|
7
|
+
|
|
8
|
+
from .client import (
|
|
9
|
+
APPROVED_STATUSES,
|
|
10
|
+
TERMINAL_STATUSES,
|
|
11
|
+
Client,
|
|
12
|
+
action_metadata,
|
|
13
|
+
create_delegation_token,
|
|
14
|
+
delegated_request_body_hash,
|
|
15
|
+
delegation_scope,
|
|
16
|
+
delegated_request_signature_payload,
|
|
17
|
+
intercept,
|
|
18
|
+
is_approved_status,
|
|
19
|
+
is_replay_error,
|
|
20
|
+
is_revoked_error,
|
|
21
|
+
is_scope_error,
|
|
22
|
+
is_terminal_status,
|
|
23
|
+
sign_delegated_request,
|
|
24
|
+
with_delegation,
|
|
25
|
+
)
|
|
26
|
+
from .dlp import DlpResult, protect_metadata
|
|
27
|
+
from .exceptions import (
|
|
28
|
+
SernixaApiError,
|
|
29
|
+
SernixaBlockedError,
|
|
30
|
+
SernixaConfigurationError,
|
|
31
|
+
SernixaDelegationScopeError,
|
|
32
|
+
SernixaError,
|
|
33
|
+
SernixaExpiredError,
|
|
34
|
+
SernixaRateLimitError,
|
|
35
|
+
SernixaReplayError,
|
|
36
|
+
SernixaRejectedError,
|
|
37
|
+
SernixaSignatureError,
|
|
38
|
+
SernixaTimeoutError,
|
|
39
|
+
SernixaValidationError,
|
|
40
|
+
)
|
|
41
|
+
from .adapters import (
|
|
42
|
+
McpInvocationContext,
|
|
43
|
+
McpToolBoundary,
|
|
44
|
+
SernixaCrewAITool,
|
|
45
|
+
SernixaLangChainTool,
|
|
46
|
+
crewai_tool,
|
|
47
|
+
langchain_tool,
|
|
48
|
+
mcp_tool,
|
|
49
|
+
secure_crewai_tool,
|
|
50
|
+
secure_langchain_tool,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
"Client",
|
|
55
|
+
"intercept",
|
|
56
|
+
"create_delegation_token",
|
|
57
|
+
"with_delegation",
|
|
58
|
+
"delegation_scope",
|
|
59
|
+
"delegated_request_signature_payload",
|
|
60
|
+
"delegated_request_body_hash",
|
|
61
|
+
"sign_delegated_request",
|
|
62
|
+
"action_metadata",
|
|
63
|
+
"protect_metadata",
|
|
64
|
+
"DlpResult",
|
|
65
|
+
"is_approved_status",
|
|
66
|
+
"is_terminal_status",
|
|
67
|
+
"is_replay_error",
|
|
68
|
+
"is_scope_error",
|
|
69
|
+
"is_revoked_error",
|
|
70
|
+
"APPROVED_STATUSES",
|
|
71
|
+
"TERMINAL_STATUSES",
|
|
72
|
+
"SernixaError",
|
|
73
|
+
"SernixaConfigurationError",
|
|
74
|
+
"SernixaValidationError",
|
|
75
|
+
"SernixaRateLimitError",
|
|
76
|
+
"SernixaBlockedError",
|
|
77
|
+
"SernixaDelegationScopeError",
|
|
78
|
+
"SernixaReplayError",
|
|
79
|
+
"SernixaSignatureError",
|
|
80
|
+
"SernixaRejectedError",
|
|
81
|
+
"SernixaExpiredError",
|
|
82
|
+
"SernixaTimeoutError",
|
|
83
|
+
"SernixaApiError",
|
|
84
|
+
"langchain_tool",
|
|
85
|
+
"secure_langchain_tool",
|
|
86
|
+
"SernixaLangChainTool",
|
|
87
|
+
"crewai_tool",
|
|
88
|
+
"secure_crewai_tool",
|
|
89
|
+
"SernixaCrewAITool",
|
|
90
|
+
"mcp_tool",
|
|
91
|
+
"McpToolBoundary",
|
|
92
|
+
"McpInvocationContext",
|
|
93
|
+
"__version__",
|
|
94
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .autogen import autogen_tool
|
|
2
|
+
from .crewai import SernixaCrewAITool, crewai_tool, secure_crewai_tool
|
|
3
|
+
from .langchain import SernixaLangChainTool, langchain_tool, secure_langchain_tool
|
|
4
|
+
from .mcp import McpInvocationContext, McpToolBoundary, mcp_tool
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"autogen_tool",
|
|
8
|
+
"crewai_tool",
|
|
9
|
+
"langchain_tool",
|
|
10
|
+
"secure_crewai_tool",
|
|
11
|
+
"secure_langchain_tool",
|
|
12
|
+
"SernixaCrewAITool",
|
|
13
|
+
"SernixaLangChainTool",
|
|
14
|
+
"McpInvocationContext",
|
|
15
|
+
"McpToolBoundary",
|
|
16
|
+
"mcp_tool",
|
|
17
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Awaitable
|
|
5
|
+
from typing import Any, Callable, TypeVar
|
|
6
|
+
|
|
7
|
+
from sernixa.client import Client
|
|
8
|
+
|
|
9
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def secure_callable(
|
|
13
|
+
*,
|
|
14
|
+
client: Client | None = None,
|
|
15
|
+
intent_id: str,
|
|
16
|
+
risk_level: str = "LOW",
|
|
17
|
+
operation_class: str = "tool_call",
|
|
18
|
+
data_sensitivity: str = "internal",
|
|
19
|
+
systems_touched: list[str] | None = None,
|
|
20
|
+
framework: str,
|
|
21
|
+
action_id: str | None = None,
|
|
22
|
+
**metadata: Any,
|
|
23
|
+
) -> Callable[[F], F]:
|
|
24
|
+
active_client = client or Client()
|
|
25
|
+
return active_client.intercept(
|
|
26
|
+
intent_id=intent_id,
|
|
27
|
+
risk_level=risk_level,
|
|
28
|
+
operation_class=operation_class,
|
|
29
|
+
data_sensitivity=data_sensitivity,
|
|
30
|
+
systems_touched=systems_touched or [framework],
|
|
31
|
+
metadata={"framework": framework, **metadata},
|
|
32
|
+
action_id=action_id,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def callable_name(value: Any, fallback: str) -> str:
|
|
37
|
+
for attr in ("name", "__name__"):
|
|
38
|
+
candidate = getattr(value, attr, None)
|
|
39
|
+
if isinstance(candidate, str) and candidate.strip():
|
|
40
|
+
return candidate.strip()
|
|
41
|
+
return fallback
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def action_id_for(framework: str, tool_name: str) -> str:
|
|
45
|
+
normalized = "".join(char if char.isalnum() or char in "._-" else "-" for char in tool_name)
|
|
46
|
+
return f"{framework}.{normalized.strip('-') or 'tool'}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def ensure_callable(value: Any, label: str) -> Callable[..., Any]:
|
|
50
|
+
if not callable(value):
|
|
51
|
+
raise TypeError(f"{label} must be callable.")
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def is_awaitable(value: Any) -> bool:
|
|
56
|
+
return inspect.isawaitable(value) or isinstance(value, Awaitable)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, TypeVar
|
|
4
|
+
|
|
5
|
+
from sernixa.client import Client
|
|
6
|
+
|
|
7
|
+
from ._common import secure_callable
|
|
8
|
+
|
|
9
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def autogen_tool(
|
|
13
|
+
*,
|
|
14
|
+
intent_id: str,
|
|
15
|
+
risk_level: str = "LOW",
|
|
16
|
+
operation_class: str = "tool_call",
|
|
17
|
+
data_sensitivity: str = "internal",
|
|
18
|
+
systems_touched: list[str] | None = None,
|
|
19
|
+
client: Client | None = None,
|
|
20
|
+
**metadata: Any,
|
|
21
|
+
) -> Callable[[F], F]:
|
|
22
|
+
"""Wrap an AutoGen function tool before registering it with an agent."""
|
|
23
|
+
return secure_callable(
|
|
24
|
+
client=client,
|
|
25
|
+
intent_id=intent_id,
|
|
26
|
+
risk_level=risk_level,
|
|
27
|
+
operation_class=operation_class,
|
|
28
|
+
data_sensitivity=data_sensitivity,
|
|
29
|
+
systems_touched=systems_touched,
|
|
30
|
+
framework="autogen",
|
|
31
|
+
**metadata,
|
|
32
|
+
)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, TypeVar
|
|
4
|
+
|
|
5
|
+
from sernixa.client import Client
|
|
6
|
+
|
|
7
|
+
from ._common import action_id_for, callable_name, secure_callable
|
|
8
|
+
|
|
9
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def crewai_tool(
|
|
13
|
+
*,
|
|
14
|
+
intent_id: str,
|
|
15
|
+
risk_level: str = "LOW",
|
|
16
|
+
operation_class: str = "tool_call",
|
|
17
|
+
data_sensitivity: str = "internal",
|
|
18
|
+
systems_touched: list[str] | None = None,
|
|
19
|
+
client: Client | None = None,
|
|
20
|
+
action_id: str | None = None,
|
|
21
|
+
**metadata: Any,
|
|
22
|
+
) -> Callable[[F], F]:
|
|
23
|
+
"""Wrap a CrewAI tool callable while preserving normal Python ergonomics."""
|
|
24
|
+
return secure_callable(
|
|
25
|
+
client=client,
|
|
26
|
+
intent_id=intent_id,
|
|
27
|
+
risk_level=risk_level,
|
|
28
|
+
operation_class=operation_class,
|
|
29
|
+
data_sensitivity=data_sensitivity,
|
|
30
|
+
systems_touched=systems_touched,
|
|
31
|
+
framework="crewai",
|
|
32
|
+
action_id=action_id,
|
|
33
|
+
**metadata,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SernixaCrewAITool:
|
|
38
|
+
"""Proxy for CrewAI-style tool objects and callables.
|
|
39
|
+
|
|
40
|
+
CrewAI tools commonly expose `run`, `_run`, or direct `__call__` execution.
|
|
41
|
+
The proxy guards those boundaries without requiring CrewAI as an SDK
|
|
42
|
+
dependency.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
tool: Any,
|
|
48
|
+
*,
|
|
49
|
+
intent_id: str,
|
|
50
|
+
risk_level: str = "LOW",
|
|
51
|
+
operation_class: str = "tool_call",
|
|
52
|
+
data_sensitivity: str = "internal",
|
|
53
|
+
systems_touched: list[str] | None = None,
|
|
54
|
+
client: Client | None = None,
|
|
55
|
+
action_id: str | None = None,
|
|
56
|
+
**metadata: Any,
|
|
57
|
+
) -> None:
|
|
58
|
+
self._tool = tool
|
|
59
|
+
self._client = client or Client()
|
|
60
|
+
self._tool_name = callable_name(tool, "crewai-tool")
|
|
61
|
+
self._config = {
|
|
62
|
+
"intent_id": intent_id,
|
|
63
|
+
"risk_level": risk_level,
|
|
64
|
+
"operation_class": operation_class,
|
|
65
|
+
"data_sensitivity": data_sensitivity,
|
|
66
|
+
"systems_touched": systems_touched or ["crewai"],
|
|
67
|
+
"metadata": {
|
|
68
|
+
"framework": "crewai",
|
|
69
|
+
"tool_name": self._tool_name,
|
|
70
|
+
**metadata,
|
|
71
|
+
},
|
|
72
|
+
"action_id": action_id or action_id_for("crewai", self._tool_name),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def name(self) -> str:
|
|
77
|
+
return self._tool_name
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def description(self) -> str | None:
|
|
81
|
+
description = getattr(self._tool, "description", None)
|
|
82
|
+
return str(description) if description is not None else None
|
|
83
|
+
|
|
84
|
+
def __getattr__(self, name: str) -> Any:
|
|
85
|
+
return getattr(self._tool, name)
|
|
86
|
+
|
|
87
|
+
def _guard(self, method_name: str, *args: Any, **kwargs: Any) -> Any:
|
|
88
|
+
method = getattr(self._tool, method_name)
|
|
89
|
+
|
|
90
|
+
@self._client.intercept(**self._config)
|
|
91
|
+
def guarded_call() -> Any:
|
|
92
|
+
return method(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
return guarded_call()
|
|
95
|
+
|
|
96
|
+
def run(self, *args: Any, **kwargs: Any) -> Any:
|
|
97
|
+
if hasattr(self._tool, "run"):
|
|
98
|
+
return self._guard("run", *args, **kwargs)
|
|
99
|
+
return self.__call__(*args, **kwargs)
|
|
100
|
+
|
|
101
|
+
def _run(self, *args: Any, **kwargs: Any) -> Any:
|
|
102
|
+
if hasattr(self._tool, "_run"):
|
|
103
|
+
return self._guard("_run", *args, **kwargs)
|
|
104
|
+
return self.__call__(*args, **kwargs)
|
|
105
|
+
|
|
106
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
107
|
+
return self._guard("__call__", *args, **kwargs)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def secure_crewai_tool(
|
|
111
|
+
tool: Any,
|
|
112
|
+
*,
|
|
113
|
+
intent_id: str,
|
|
114
|
+
risk_level: str = "LOW",
|
|
115
|
+
operation_class: str = "tool_call",
|
|
116
|
+
data_sensitivity: str = "internal",
|
|
117
|
+
systems_touched: list[str] | None = None,
|
|
118
|
+
client: Client | None = None,
|
|
119
|
+
action_id: str | None = None,
|
|
120
|
+
**metadata: Any,
|
|
121
|
+
) -> SernixaCrewAITool:
|
|
122
|
+
"""Wrap an existing CrewAI tool object/callable with Sernixa."""
|
|
123
|
+
return SernixaCrewAITool(
|
|
124
|
+
tool,
|
|
125
|
+
intent_id=intent_id,
|
|
126
|
+
risk_level=risk_level,
|
|
127
|
+
operation_class=operation_class,
|
|
128
|
+
data_sensitivity=data_sensitivity,
|
|
129
|
+
systems_touched=systems_touched,
|
|
130
|
+
client=client,
|
|
131
|
+
action_id=action_id,
|
|
132
|
+
**metadata,
|
|
133
|
+
)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, TypeVar
|
|
4
|
+
|
|
5
|
+
from sernixa.client import Client
|
|
6
|
+
|
|
7
|
+
from ._common import action_id_for, callable_name, secure_callable
|
|
8
|
+
|
|
9
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def langchain_tool(
|
|
13
|
+
*,
|
|
14
|
+
intent_id: str,
|
|
15
|
+
risk_level: str = "LOW",
|
|
16
|
+
operation_class: str = "tool_call",
|
|
17
|
+
data_sensitivity: str = "internal",
|
|
18
|
+
systems_touched: list[str] | None = None,
|
|
19
|
+
client: Client | None = None,
|
|
20
|
+
action_id: str | None = None,
|
|
21
|
+
**metadata: Any,
|
|
22
|
+
) -> Callable[[F], F]:
|
|
23
|
+
"""Wrap a LangChain tool function before or after `@tool` decoration."""
|
|
24
|
+
return secure_callable(
|
|
25
|
+
client=client,
|
|
26
|
+
intent_id=intent_id,
|
|
27
|
+
risk_level=risk_level,
|
|
28
|
+
operation_class=operation_class,
|
|
29
|
+
data_sensitivity=data_sensitivity,
|
|
30
|
+
systems_touched=systems_touched,
|
|
31
|
+
framework="langchain",
|
|
32
|
+
action_id=action_id,
|
|
33
|
+
**metadata,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SernixaLangChainTool:
|
|
38
|
+
"""Proxy for LangChain-style tools.
|
|
39
|
+
|
|
40
|
+
The proxy intentionally depends only on LangChain's public calling shapes
|
|
41
|
+
(`invoke`, `ainvoke`, `run`, `arun`, and `__call__`) so the SDK remains
|
|
42
|
+
usable without importing LangChain at runtime.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
tool: Any,
|
|
48
|
+
*,
|
|
49
|
+
intent_id: str,
|
|
50
|
+
risk_level: str = "LOW",
|
|
51
|
+
operation_class: str = "tool_call",
|
|
52
|
+
data_sensitivity: str = "internal",
|
|
53
|
+
systems_touched: list[str] | None = None,
|
|
54
|
+
client: Client | None = None,
|
|
55
|
+
action_id: str | None = None,
|
|
56
|
+
**metadata: Any,
|
|
57
|
+
) -> None:
|
|
58
|
+
self._tool = tool
|
|
59
|
+
self._client = client or Client()
|
|
60
|
+
self._tool_name = callable_name(tool, "langchain-tool")
|
|
61
|
+
self._config = {
|
|
62
|
+
"intent_id": intent_id,
|
|
63
|
+
"risk_level": risk_level,
|
|
64
|
+
"operation_class": operation_class,
|
|
65
|
+
"data_sensitivity": data_sensitivity,
|
|
66
|
+
"systems_touched": systems_touched or ["langchain"],
|
|
67
|
+
"metadata": {
|
|
68
|
+
"framework": "langchain",
|
|
69
|
+
"tool_name": self._tool_name,
|
|
70
|
+
**metadata,
|
|
71
|
+
},
|
|
72
|
+
"action_id": action_id or action_id_for("langchain", self._tool_name),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def name(self) -> str:
|
|
77
|
+
return self._tool_name
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def description(self) -> str | None:
|
|
81
|
+
description = getattr(self._tool, "description", None)
|
|
82
|
+
return str(description) if description is not None else None
|
|
83
|
+
|
|
84
|
+
def __getattr__(self, name: str) -> Any:
|
|
85
|
+
return getattr(self._tool, name)
|
|
86
|
+
|
|
87
|
+
def _guard(self, method_name: str, *args: Any, **kwargs: Any) -> Any:
|
|
88
|
+
method = getattr(self._tool, method_name)
|
|
89
|
+
|
|
90
|
+
@self._client.intercept(**self._config)
|
|
91
|
+
def guarded_call() -> Any:
|
|
92
|
+
return method(*args, **kwargs)
|
|
93
|
+
|
|
94
|
+
return guarded_call()
|
|
95
|
+
|
|
96
|
+
async def _aguard(self, method_name: str, *args: Any, **kwargs: Any) -> Any:
|
|
97
|
+
method = getattr(self._tool, method_name)
|
|
98
|
+
|
|
99
|
+
@self._client.intercept(**self._config)
|
|
100
|
+
async def guarded_call() -> Any:
|
|
101
|
+
return await method(*args, **kwargs)
|
|
102
|
+
|
|
103
|
+
return await guarded_call()
|
|
104
|
+
|
|
105
|
+
def invoke(self, input: Any, config: Any | None = None, **kwargs: Any) -> Any:
|
|
106
|
+
return self._guard("invoke", input, config=config, **kwargs)
|
|
107
|
+
|
|
108
|
+
async def ainvoke(self, input: Any, config: Any | None = None, **kwargs: Any) -> Any:
|
|
109
|
+
return await self._aguard("ainvoke", input, config=config, **kwargs)
|
|
110
|
+
|
|
111
|
+
def run(self, *args: Any, **kwargs: Any) -> Any:
|
|
112
|
+
return self._guard("run", *args, **kwargs)
|
|
113
|
+
|
|
114
|
+
async def arun(self, *args: Any, **kwargs: Any) -> Any:
|
|
115
|
+
return await self._aguard("arun", *args, **kwargs)
|
|
116
|
+
|
|
117
|
+
def __call__(self, *args: Any, **kwargs: Any) -> Any:
|
|
118
|
+
if hasattr(self._tool, "invoke"):
|
|
119
|
+
return self.invoke(*args, **kwargs)
|
|
120
|
+
return self._guard("__call__", *args, **kwargs)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def secure_langchain_tool(
|
|
124
|
+
tool: Any,
|
|
125
|
+
*,
|
|
126
|
+
intent_id: str,
|
|
127
|
+
risk_level: str = "LOW",
|
|
128
|
+
operation_class: str = "tool_call",
|
|
129
|
+
data_sensitivity: str = "internal",
|
|
130
|
+
systems_touched: list[str] | None = None,
|
|
131
|
+
client: Client | None = None,
|
|
132
|
+
action_id: str | None = None,
|
|
133
|
+
**metadata: Any,
|
|
134
|
+
) -> SernixaLangChainTool:
|
|
135
|
+
"""Wrap an existing LangChain BaseTool/Runnable-style object with Sernixa."""
|
|
136
|
+
return SernixaLangChainTool(
|
|
137
|
+
tool,
|
|
138
|
+
intent_id=intent_id,
|
|
139
|
+
risk_level=risk_level,
|
|
140
|
+
operation_class=operation_class,
|
|
141
|
+
data_sensitivity=data_sensitivity,
|
|
142
|
+
systems_touched=systems_touched,
|
|
143
|
+
client=client,
|
|
144
|
+
action_id=action_id,
|
|
145
|
+
**metadata,
|
|
146
|
+
)
|
sernixa/adapters/mcp.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Callable, TypeVar
|
|
5
|
+
|
|
6
|
+
from sernixa.client import Client, safe_serialize
|
|
7
|
+
|
|
8
|
+
from ._common import action_id_for, is_awaitable, secure_callable
|
|
9
|
+
|
|
10
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class McpInvocationContext:
|
|
15
|
+
"""Metadata Sernixa records at an MCP-style tool boundary.
|
|
16
|
+
|
|
17
|
+
This is not a full MCP protocol server. It is a small, explicit shape for
|
|
18
|
+
guarding the moment a host or agent invokes a tool exposed through MCP-like
|
|
19
|
+
routing.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
server_name: str
|
|
23
|
+
tool_name: str
|
|
24
|
+
toolset_id: str | None = None
|
|
25
|
+
client_name: str | None = None
|
|
26
|
+
session_id: str | None = None
|
|
27
|
+
request_id: str | None = None
|
|
28
|
+
resource_hints: list[str] = field(default_factory=list)
|
|
29
|
+
|
|
30
|
+
def to_metadata(self, arguments: Any | None = None) -> dict[str, Any]:
|
|
31
|
+
metadata: dict[str, Any] = {
|
|
32
|
+
"mcp_server": self.server_name,
|
|
33
|
+
"mcp_tool": self.tool_name,
|
|
34
|
+
"mcp_toolset_id": self.toolset_id,
|
|
35
|
+
"mcp_client": self.client_name,
|
|
36
|
+
"mcp_session_id": self.session_id,
|
|
37
|
+
"mcp_request_id": self.request_id,
|
|
38
|
+
"mcp_resource_hints": list(self.resource_hints),
|
|
39
|
+
}
|
|
40
|
+
if arguments is not None:
|
|
41
|
+
metadata["mcp_arguments"] = safe_serialize(arguments)
|
|
42
|
+
return {key: value for key, value in metadata.items() if value not in (None, [], {})}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def mcp_tool(
|
|
46
|
+
*,
|
|
47
|
+
intent_id: str,
|
|
48
|
+
server_name: str,
|
|
49
|
+
tool_name: str,
|
|
50
|
+
risk_level: str = "LOW",
|
|
51
|
+
operation_class: str = "tool_call",
|
|
52
|
+
data_sensitivity: str = "internal",
|
|
53
|
+
systems_touched: list[str] | None = None,
|
|
54
|
+
client: Client | None = None,
|
|
55
|
+
toolset_id: str | None = None,
|
|
56
|
+
**metadata: Any,
|
|
57
|
+
) -> Callable[[F], F]:
|
|
58
|
+
"""Decorate an MCP-style tool handler with Sernixa governance."""
|
|
59
|
+
context = McpInvocationContext(
|
|
60
|
+
server_name=server_name,
|
|
61
|
+
tool_name=tool_name,
|
|
62
|
+
toolset_id=toolset_id,
|
|
63
|
+
)
|
|
64
|
+
return secure_callable(
|
|
65
|
+
client=client,
|
|
66
|
+
intent_id=intent_id,
|
|
67
|
+
risk_level=risk_level,
|
|
68
|
+
operation_class=operation_class,
|
|
69
|
+
data_sensitivity=data_sensitivity,
|
|
70
|
+
systems_touched=systems_touched or [server_name],
|
|
71
|
+
framework="mcp",
|
|
72
|
+
action_id=action_id_for("mcp", f"{server_name}.{tool_name}"),
|
|
73
|
+
**context.to_metadata(),
|
|
74
|
+
**metadata,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class McpToolBoundary:
|
|
79
|
+
"""Guard explicit MCP-style tool invocations.
|
|
80
|
+
|
|
81
|
+
Use this at the MCP host/router boundary, before dispatching a tool call to
|
|
82
|
+
the underlying tool implementation.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
client: Client | None = None,
|
|
89
|
+
server_name: str,
|
|
90
|
+
toolset_id: str | None = None,
|
|
91
|
+
systems_touched: list[str] | None = None,
|
|
92
|
+
) -> None:
|
|
93
|
+
self.client = client or Client()
|
|
94
|
+
self.server_name = server_name
|
|
95
|
+
self.toolset_id = toolset_id
|
|
96
|
+
self.systems_touched = systems_touched or [server_name]
|
|
97
|
+
|
|
98
|
+
def invoke(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
tool_name: str,
|
|
102
|
+
arguments: dict[str, Any] | None,
|
|
103
|
+
handler: Callable[..., Any],
|
|
104
|
+
intent_id: str,
|
|
105
|
+
risk_level: str = "LOW",
|
|
106
|
+
operation_class: str = "tool_call",
|
|
107
|
+
data_sensitivity: str = "internal",
|
|
108
|
+
client_name: str | None = None,
|
|
109
|
+
session_id: str | None = None,
|
|
110
|
+
request_id: str | None = None,
|
|
111
|
+
resource_hints: list[str] | None = None,
|
|
112
|
+
**metadata: Any,
|
|
113
|
+
) -> Any:
|
|
114
|
+
context = McpInvocationContext(
|
|
115
|
+
server_name=self.server_name,
|
|
116
|
+
tool_name=tool_name,
|
|
117
|
+
toolset_id=self.toolset_id,
|
|
118
|
+
client_name=client_name,
|
|
119
|
+
session_id=session_id,
|
|
120
|
+
request_id=request_id,
|
|
121
|
+
resource_hints=resource_hints or [],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
@self.client.intercept(
|
|
125
|
+
intent_id=intent_id,
|
|
126
|
+
risk_level=risk_level,
|
|
127
|
+
operation_class=operation_class,
|
|
128
|
+
data_sensitivity=data_sensitivity,
|
|
129
|
+
systems_touched=self.systems_touched,
|
|
130
|
+
metadata={"framework": "mcp", **context.to_metadata(arguments), **metadata},
|
|
131
|
+
action_id=action_id_for("mcp", f"{self.server_name}.{tool_name}"),
|
|
132
|
+
)
|
|
133
|
+
def guarded_call() -> Any:
|
|
134
|
+
return handler(**(arguments or {}))
|
|
135
|
+
|
|
136
|
+
return guarded_call()
|
|
137
|
+
|
|
138
|
+
async def ainvoke(
|
|
139
|
+
self,
|
|
140
|
+
*,
|
|
141
|
+
tool_name: str,
|
|
142
|
+
arguments: dict[str, Any] | None,
|
|
143
|
+
handler: Callable[..., Any],
|
|
144
|
+
intent_id: str,
|
|
145
|
+
risk_level: str = "LOW",
|
|
146
|
+
operation_class: str = "tool_call",
|
|
147
|
+
data_sensitivity: str = "internal",
|
|
148
|
+
client_name: str | None = None,
|
|
149
|
+
session_id: str | None = None,
|
|
150
|
+
request_id: str | None = None,
|
|
151
|
+
resource_hints: list[str] | None = None,
|
|
152
|
+
**metadata: Any,
|
|
153
|
+
) -> Any:
|
|
154
|
+
context = McpInvocationContext(
|
|
155
|
+
server_name=self.server_name,
|
|
156
|
+
tool_name=tool_name,
|
|
157
|
+
toolset_id=self.toolset_id,
|
|
158
|
+
client_name=client_name,
|
|
159
|
+
session_id=session_id,
|
|
160
|
+
request_id=request_id,
|
|
161
|
+
resource_hints=resource_hints or [],
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@self.client.intercept(
|
|
165
|
+
intent_id=intent_id,
|
|
166
|
+
risk_level=risk_level,
|
|
167
|
+
operation_class=operation_class,
|
|
168
|
+
data_sensitivity=data_sensitivity,
|
|
169
|
+
systems_touched=self.systems_touched,
|
|
170
|
+
metadata={"framework": "mcp", **context.to_metadata(arguments), **metadata},
|
|
171
|
+
action_id=action_id_for("mcp", f"{self.server_name}.{tool_name}"),
|
|
172
|
+
)
|
|
173
|
+
async def guarded_call() -> Any:
|
|
174
|
+
result = handler(**(arguments or {}))
|
|
175
|
+
if is_awaitable(result):
|
|
176
|
+
return await result
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
return await guarded_call()
|