common-core 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.
- common_core/__init__.py +54 -0
- common_core/auth.py +306 -0
- common_core/config.py +842 -0
- common_core/context.py +45 -0
- common_core/instrumentation.py +70 -0
- common_core/mcp_auth.py +319 -0
- common_core/observability.py +383 -0
- common_core/protocols.py +162 -0
- common_core/providers/__init__.py +21 -0
- common_core/providers/cache.py +266 -0
- common_core/providers/llm.py +272 -0
- common_core/providers/vector.py +440 -0
- common_core/rag/__init__.py +60 -0
- common_core/rag/assembly.py +496 -0
- common_core/rag/generation.py +177 -0
- common_core/rag/guard.py +302 -0
- common_core/security.py +124 -0
- common_core/telemetry.py +214 -0
- common_core-0.1.0.dist-info/METADATA +19 -0
- common_core-0.1.0.dist-info/RECORD +21 -0
- common_core-0.1.0.dist-info/WHEEL +4 -0
common_core/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""共用运行时(common_core):供多个 skill 共享的底层组件集合。
|
|
2
|
+
本包不依赖具体业务,提供配置、鉴权、上下文、可观测性、安全审查与数据提供者的基础组件,是各 skill 的地基。
|
|
3
|
+
导出名用于方便业务模块从包顶层直接引入常用类与函数。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .auth import AuthError, IdentityClaims, TokenVerifier
|
|
7
|
+
from .config import (
|
|
8
|
+
AuthConfig,
|
|
9
|
+
CacheConfig,
|
|
10
|
+
LLMConfig,
|
|
11
|
+
MetricsConfig,
|
|
12
|
+
RetrievalConfig,
|
|
13
|
+
RuntimeConfig,
|
|
14
|
+
VectorStoreConfig,
|
|
15
|
+
)
|
|
16
|
+
from .context import AgentContext
|
|
17
|
+
from .mcp_auth import (
|
|
18
|
+
MCPBearerTokenVerifier,
|
|
19
|
+
ToolAuthError,
|
|
20
|
+
ToolContextGuard,
|
|
21
|
+
build_mcp_auth,
|
|
22
|
+
resolve_tool_context,
|
|
23
|
+
)
|
|
24
|
+
from .observability import Observability
|
|
25
|
+
from .protocols import QueryRequest, QueryResult
|
|
26
|
+
from .security import INJECTION_PATTERNS, check_safety, mask_pii, normalize_query
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"AuthConfig",
|
|
32
|
+
"AuthError",
|
|
33
|
+
"AgentContext",
|
|
34
|
+
"CacheConfig",
|
|
35
|
+
"INJECTION_PATTERNS",
|
|
36
|
+
"IdentityClaims",
|
|
37
|
+
"LLMConfig",
|
|
38
|
+
"MCPBearerTokenVerifier",
|
|
39
|
+
"MetricsConfig",
|
|
40
|
+
"Observability",
|
|
41
|
+
"QueryRequest",
|
|
42
|
+
"QueryResult",
|
|
43
|
+
"RetrievalConfig",
|
|
44
|
+
"RuntimeConfig",
|
|
45
|
+
"ToolAuthError",
|
|
46
|
+
"ToolContextGuard",
|
|
47
|
+
"TokenVerifier",
|
|
48
|
+
"VectorStoreConfig",
|
|
49
|
+
"build_mcp_auth",
|
|
50
|
+
"check_safety",
|
|
51
|
+
"mask_pii",
|
|
52
|
+
"normalize_query",
|
|
53
|
+
"resolve_tool_context",
|
|
54
|
+
]
|
common_core/auth.py
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""与框架解耦的 JWT 身份校验模块。本模块刻意不引入 langgraph_sdk,也不依赖任何 Web 服务框架。
|
|
2
|
+
在 LangGraph 内部运行的业务模块,可以把这套基础能力接入自身的鉴权装饰器;普通独立服务可以直接使用 TokenVerifier 类。
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import base64
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Mapping
|
|
10
|
+
|
|
11
|
+
from .config import AuthConfig
|
|
12
|
+
from .context import AgentContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AuthError(Exception):
|
|
16
|
+
"""当 token 缺失、格式错误、已过期或无效时抛出的异常,带有 HTTP 状态码以便上层转换为相应响应。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str, status_code: int = 401) -> None:
|
|
20
|
+
"""初始化鉴权异常。
|
|
21
|
+
参数:
|
|
22
|
+
message: 人可读的错误信息。
|
|
23
|
+
status_code: 应返回的 HTTP 状态码,默认 401。
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(message)
|
|
26
|
+
self.message = message
|
|
27
|
+
self.status_code = status_code
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_bearer_token(authorization: str | None) -> str | None:
|
|
31
|
+
"""从 Authorization 头中提取 Bearer token。
|
|
32
|
+
参数:
|
|
33
|
+
authorization: HTTP Authorization 头的原始值(可为 None)。
|
|
34
|
+
返回:
|
|
35
|
+
成功时返回 token 字符串;头缺失/格式不正(非 Bearer 或空)时返回 None。
|
|
36
|
+
"""
|
|
37
|
+
if not authorization:
|
|
38
|
+
return None
|
|
39
|
+
parts = authorization.split(" ", 1)
|
|
40
|
+
if (
|
|
41
|
+
len(parts) == 2
|
|
42
|
+
and parts[0].strip().lower() == "bearer"
|
|
43
|
+
and parts[1].strip()
|
|
44
|
+
):
|
|
45
|
+
return parts[1].strip()
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def decode_public_key(key: str) -> str:
|
|
50
|
+
"""返回 PEM 文本,当输入是被 base64 包裹的 PEM 时自动解码。
|
|
51
|
+
参数:
|
|
52
|
+
key: 原始密钥,可能是 PEM 文本或 base64 字符串。
|
|
53
|
+
返回:
|
|
54
|
+
解码后的 PEM 文本;若不是可解码的 base64 PEM,原样返回输入。
|
|
55
|
+
"""
|
|
56
|
+
if "-----BEGIN" in key:
|
|
57
|
+
return key
|
|
58
|
+
try:
|
|
59
|
+
decoded = base64.b64decode(key).decode("utf-8")
|
|
60
|
+
except Exception:
|
|
61
|
+
return key
|
|
62
|
+
return decoded if "-----BEGIN" in decoded else key
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class IdentityClaims:
|
|
67
|
+
"""鉴权后的身份声明集合,封装了用户与分层边界信息。
|
|
68
|
+
属性:
|
|
69
|
+
sub: 主体,通常代表用户 ID。
|
|
70
|
+
tenant_id: 租户 ID,数据隔离边界。
|
|
71
|
+
kb_id: 知识库 ID。
|
|
72
|
+
session_id: 会话 ID。
|
|
73
|
+
request_id: 请求 ID。
|
|
74
|
+
roles: 角色/范围集合,可来自 roles/scope/scp 声明。
|
|
75
|
+
extra: 其他未知自定义声明的保留字典。
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
sub: str
|
|
79
|
+
tenant_id: str = ""
|
|
80
|
+
kb_id: str = ""
|
|
81
|
+
session_id: str = ""
|
|
82
|
+
request_id: str = ""
|
|
83
|
+
roles: tuple[str, ...] = ()
|
|
84
|
+
extra: dict[str, Any] = field(default_factory=dict)
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_payload(cls, payload: Mapping[str, Any]) -> "IdentityClaims":
|
|
88
|
+
"""从解码后的 JWT payload 构造 IdentityClaims。
|
|
89
|
+
参数:
|
|
90
|
+
payload: pyjwt 解码出的字典。
|
|
91
|
+
返回:
|
|
92
|
+
填充好身份信息的 IdentityClaims 实例。
|
|
93
|
+
"""
|
|
94
|
+
raw_roles = (
|
|
95
|
+
payload.get("roles") or payload.get("scope") or payload.get("scp") or ()
|
|
96
|
+
)
|
|
97
|
+
if isinstance(raw_roles, str):
|
|
98
|
+
roles = tuple(item.strip() for item in raw_roles.split() if item.strip())
|
|
99
|
+
else:
|
|
100
|
+
roles = tuple(str(item) for item in raw_roles)
|
|
101
|
+
|
|
102
|
+
extra = {
|
|
103
|
+
key: value
|
|
104
|
+
for key, value in payload.items()
|
|
105
|
+
if key
|
|
106
|
+
not in {
|
|
107
|
+
"sub",
|
|
108
|
+
"tenant_id",
|
|
109
|
+
"kb_id",
|
|
110
|
+
"session_id",
|
|
111
|
+
"request_id",
|
|
112
|
+
"roles",
|
|
113
|
+
"scope",
|
|
114
|
+
"scp",
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return cls(
|
|
118
|
+
sub=str(payload.get("sub") or ""),
|
|
119
|
+
tenant_id=str(payload.get("tenant_id") or ""),
|
|
120
|
+
kb_id=str(payload.get("kb_id") or ""),
|
|
121
|
+
session_id=str(payload.get("session_id") or ""),
|
|
122
|
+
request_id=str(payload.get("request_id") or ""),
|
|
123
|
+
roles=roles,
|
|
124
|
+
extra=extra,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def to_context(self) -> AgentContext:
|
|
128
|
+
"""转换为 AgentContext,用于在整个处理链路中传递身份与隔离边界。
|
|
129
|
+
返回:
|
|
130
|
+
以 sub 作为 user_id、其余字段对应填充 的 AgentContext。
|
|
131
|
+
"""
|
|
132
|
+
return AgentContext(
|
|
133
|
+
tenant_id=self.tenant_id,
|
|
134
|
+
kb_id=self.kb_id,
|
|
135
|
+
session_id=self.session_id,
|
|
136
|
+
request_id=self.request_id,
|
|
137
|
+
user_id=self.sub,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TokenVerifier:
|
|
142
|
+
"""使用 HS256/RS256/ES256 校验 JWT,支持静态密钥、静态公钥或 JWKS 端点。
|
|
143
|
+
|
|
144
|
+
密钥选择优先级:显式 ``key`` 参数 > ``jwt_secret`` > ``jwt_public_key``
|
|
145
|
+
> ``jwt_jwks_url``。JWKS 模式会按 ``AUTH_JWKS_LIFESPAN`` 缓存公钥集合,
|
|
146
|
+
遇到未知 ``kid`` 时强制重新拉取,因此 IdP 轮换签名密钥时无需重启服务进程。
|
|
147
|
+
支持可选的 iss、aud 校验。
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
config: AuthConfig | None = None,
|
|
153
|
+
*,
|
|
154
|
+
key: str | None = None,
|
|
155
|
+
algorithms: tuple[str, ...] | list[str] | None = None,
|
|
156
|
+
issuer: str | None = None,
|
|
157
|
+
audience: str | None = None,
|
|
158
|
+
jwks_url: str | None = None,
|
|
159
|
+
jwks_lifespan: float | None = None,
|
|
160
|
+
jwks_timeout: float | None = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""初始化 token 校验器。
|
|
163
|
+
参数:
|
|
164
|
+
config: JWT 相关配置,包含密钥、算法、issuer/audience 等。
|
|
165
|
+
key: 显式提供的密钥;优先级高于 config 中的密钥。
|
|
166
|
+
algorithms: 允许的签名算法集合;None 时用 config.jwt_algorithms。
|
|
167
|
+
issuer: 必须匹配的发行方;None 时用 config。
|
|
168
|
+
audience: 必须匹配的受众;None 时用 config。
|
|
169
|
+
jwks_url: JWKS 端点 URL;优先级低于静态密钥,None 时用 config。
|
|
170
|
+
jwks_lifespan: JWKS 集合缓存秒数;None 时用 config(默认 300)。
|
|
171
|
+
jwks_timeout: 拉取 JWKS 的 HTTP 超时秒数;None 时用 config(默认 30)。
|
|
172
|
+
"""
|
|
173
|
+
cfg = config or AuthConfig()
|
|
174
|
+
self.config = cfg
|
|
175
|
+
explicit_algorithms = algorithms is not None
|
|
176
|
+
self._jwks_url = jwks_url if jwks_url is not None else cfg.jwt_jwks_url
|
|
177
|
+
self._jwks_lifespan = (
|
|
178
|
+
jwks_lifespan if jwks_lifespan is not None else cfg.jwt_jwks_lifespan
|
|
179
|
+
)
|
|
180
|
+
self._jwks_timeout = (
|
|
181
|
+
jwks_timeout if jwks_timeout is not None else cfg.jwt_jwks_timeout
|
|
182
|
+
)
|
|
183
|
+
self._jwks_client: Any | None = None
|
|
184
|
+
if key is not None:
|
|
185
|
+
self._key = key
|
|
186
|
+
elif cfg.jwt_secret:
|
|
187
|
+
self._key = cfg.jwt_secret
|
|
188
|
+
elif cfg.jwt_public_key:
|
|
189
|
+
self._key = decode_public_key(cfg.jwt_public_key)
|
|
190
|
+
else:
|
|
191
|
+
self._key = ""
|
|
192
|
+
self._algorithms = tuple(algorithms or cfg.jwt_algorithms)
|
|
193
|
+
if (
|
|
194
|
+
self._jwks_url
|
|
195
|
+
and not explicit_algorithms
|
|
196
|
+
and self._algorithms == ("HS256",)
|
|
197
|
+
):
|
|
198
|
+
# 直连构造 AuthConfig(jwt_jwks_url=...) 时,若没显式给算法,
|
|
199
|
+
# 自动切到 JWKS 通常使用的非对称算法,避免 HS256 误配。
|
|
200
|
+
self._algorithms = ("RS256", "ES256")
|
|
201
|
+
self._issuer = cfg.jwt_issuer if issuer is None else issuer
|
|
202
|
+
self._audience = cfg.jwt_audience if audience is None else audience
|
|
203
|
+
|
|
204
|
+
def _jwk_client(self) -> Any:
|
|
205
|
+
"""返回复用的 PyJWKClient 实例(首次使用时惰性创建)。
|
|
206
|
+
|
|
207
|
+
PyJWKClient 内部有两层缓存:JWK 集合按 lifespan TTL 过期;未知 kid
|
|
208
|
+
时会强制刷新集合再查找,保证 IdP 轮换密钥后立即可用。
|
|
209
|
+
|
|
210
|
+
返回:
|
|
211
|
+
PyJWT 的 PyJWKClient 实例。
|
|
212
|
+
"""
|
|
213
|
+
if not self._jwks_url:
|
|
214
|
+
raise AuthError("JWKS URL is not configured; refusing to verify tokens.")
|
|
215
|
+
if self._jwks_client is None:
|
|
216
|
+
from jwt import PyJWKClient
|
|
217
|
+
|
|
218
|
+
self._jwks_client = PyJWKClient(
|
|
219
|
+
self._jwks_url,
|
|
220
|
+
cache_jwk_set=True,
|
|
221
|
+
lifespan=self._jwks_lifespan,
|
|
222
|
+
timeout=self._jwks_timeout,
|
|
223
|
+
)
|
|
224
|
+
return self._jwks_client
|
|
225
|
+
|
|
226
|
+
def _jwks_signing_key(self, token: str) -> Any:
|
|
227
|
+
"""按 token 的 kid 从 JWKS 取签名公钥;无 kid 时回退到集合中第一把签名钥。"""
|
|
228
|
+
import jwt as pyjwt
|
|
229
|
+
|
|
230
|
+
header = pyjwt.get_unverified_header(token)
|
|
231
|
+
client = self._jwk_client()
|
|
232
|
+
kid = header.get("kid")
|
|
233
|
+
if kid:
|
|
234
|
+
return client.get_signing_key(str(kid))
|
|
235
|
+
keys = client.get_signing_keys()
|
|
236
|
+
if not keys:
|
|
237
|
+
raise AuthError("JWKS endpoint did not publish any signing keys.", 401)
|
|
238
|
+
return keys[0]
|
|
239
|
+
|
|
240
|
+
def verify(self, token: str) -> dict[str, Any]:
|
|
241
|
+
"""校验并解码 JWT。
|
|
242
|
+
参数:
|
|
243
|
+
token: 待校验的 JWT 字符串。
|
|
244
|
+
返回:
|
|
245
|
+
解码后的 payload 字典。
|
|
246
|
+
异常:
|
|
247
|
+
AuthError: 密钥/JWKS 未配置、JWKS 拉取失败、token 过期或无效时抛出。
|
|
248
|
+
"""
|
|
249
|
+
has_static_key = bool(self._key)
|
|
250
|
+
has_jwks = bool(self._jwks_url)
|
|
251
|
+
if not has_static_key and not has_jwks:
|
|
252
|
+
raise AuthError("Auth key is not configured; refusing to verify tokens.")
|
|
253
|
+
|
|
254
|
+
import jwt as pyjwt
|
|
255
|
+
|
|
256
|
+
kwargs: dict[str, Any] = {"algorithms": self._algorithms, "options": {"require": ["exp"]}}
|
|
257
|
+
if self._issuer:
|
|
258
|
+
kwargs["issuer"] = self._issuer
|
|
259
|
+
if self._audience:
|
|
260
|
+
kwargs["audience"] = self._audience
|
|
261
|
+
if has_jwks and not has_static_key:
|
|
262
|
+
try:
|
|
263
|
+
signing_key = self._jwks_signing_key(token)
|
|
264
|
+
key = signing_key.key
|
|
265
|
+
except AuthError:
|
|
266
|
+
raise
|
|
267
|
+
except Exception as exc:
|
|
268
|
+
raise AuthError(f"Invalid token: {exc}") from None
|
|
269
|
+
else:
|
|
270
|
+
key = self._key
|
|
271
|
+
try:
|
|
272
|
+
return pyjwt.decode(token, key, **kwargs)
|
|
273
|
+
except pyjwt.ExpiredSignatureError:
|
|
274
|
+
raise AuthError("Token expired.") from None
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
raise AuthError(f"Invalid token: {exc}") from None
|
|
277
|
+
|
|
278
|
+
def identity(self, token: str) -> IdentityClaims:
|
|
279
|
+
"""校验 token 并转换为 IdentityClaims。
|
|
280
|
+
返回:
|
|
281
|
+
身份声明对象。
|
|
282
|
+
"""
|
|
283
|
+
return IdentityClaims.from_payload(self.verify(token))
|
|
284
|
+
|
|
285
|
+
def context(self, token: str) -> AgentContext:
|
|
286
|
+
"""校验 token 并直接转换为 AgentContext。
|
|
287
|
+
返回:
|
|
288
|
+
包含身份与隔离边界的 AgentContext。
|
|
289
|
+
"""
|
|
290
|
+
return self.identity(token).to_context()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def verify_token(
|
|
294
|
+
token: str,
|
|
295
|
+
config: AuthConfig | None = None,
|
|
296
|
+
**kwargs: Any,
|
|
297
|
+
) -> IdentityClaims:
|
|
298
|
+
"""快捷口:校验 token 并返回 IdentityClaims。
|
|
299
|
+
参数:
|
|
300
|
+
token: 待校验的 JWT。
|
|
301
|
+
config: 可选配置,None 时使用默认值。
|
|
302
|
+
**kwargs: 其他传递给 TokenVerifier 的参数。
|
|
303
|
+
返回:
|
|
304
|
+
身份声明对象。
|
|
305
|
+
"""
|
|
306
|
+
return TokenVerifier(config, **kwargs).identity(token)
|