theseus-kit 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.
- theseus_kit/__init__.py +126 -0
- theseus_kit/__main__.py +4 -0
- theseus_kit/config.py +155 -0
- theseus_kit/credentials.py +60 -0
- theseus_kit/errors.py +166 -0
- theseus_kit/models.py +494 -0
- theseus_kit/oauth.py +134 -0
- theseus_kit/redaction.py +150 -0
- theseus_kit/resources.py +67 -0
- theseus_kit/routing.py +55 -0
- theseus_kit/server.py +555 -0
- theseus_kit/services/__init__.py +19 -0
- theseus_kit/services/config_reader.py +939 -0
- theseus_kit/services/draft_creator.py +93 -0
- theseus_kit/services/draft_editor.py +118 -0
- theseus_kit/services/draft_validator.py +75 -0
- theseus_kit/services/llms_doc_reader.py +114 -0
- theseus_kit/services/publisher.py +137 -0
- theseus_kit/services/template_saver.py +118 -0
- theseus_kit/skills/__init__.py +46 -0
- theseus_kit/skills/_analyze_config.py +215 -0
- theseus_kit/skills/_content.py +10 -0
- theseus_kit/skills/_manage_topology.py +164 -0
- theseus_kit/skills/_publish_config.py +140 -0
- theseus_kit/skills/_registry.py +93 -0
- theseus_kit/skills/_save_template.py +111 -0
- theseus_kit/skills/_tune_config.py +271 -0
- theseus_kit/tokens.py +54 -0
- theseus_kit/transport.py +408 -0
- theseus_kit-0.1.0.dist-info/METADATA +392 -0
- theseus_kit-0.1.0.dist-info/RECORD +34 -0
- theseus_kit-0.1.0.dist-info/WHEEL +4 -0
- theseus_kit-0.1.0.dist-info/entry_points.txt +2 -0
- theseus_kit-0.1.0.dist-info/licenses/LICENSE +22 -0
theseus_kit/__init__.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""theseus-kit MCP server."""
|
|
2
|
+
|
|
3
|
+
from theseus_kit.config import (
|
|
4
|
+
CredentialConfig,
|
|
5
|
+
OAuthConfig,
|
|
6
|
+
RobotTarget,
|
|
7
|
+
TheseusSettings,
|
|
8
|
+
UserPatConfig,
|
|
9
|
+
)
|
|
10
|
+
from theseus_kit.errors import (
|
|
11
|
+
AuthRejectedError,
|
|
12
|
+
ConfigError,
|
|
13
|
+
ConfigLocatorError,
|
|
14
|
+
CredentialError,
|
|
15
|
+
DraftConflictError,
|
|
16
|
+
DraftNotFoundError,
|
|
17
|
+
ExchangeUnavailableError,
|
|
18
|
+
LlmsDocError,
|
|
19
|
+
PublishNotConfirmedError,
|
|
20
|
+
PublishPreCheckError,
|
|
21
|
+
RobotApiError,
|
|
22
|
+
RobotValidationError,
|
|
23
|
+
RoutingConfigError,
|
|
24
|
+
ScopeOrAudienceError,
|
|
25
|
+
SubscriptionFrozenError,
|
|
26
|
+
TheseusError,
|
|
27
|
+
)
|
|
28
|
+
from theseus_kit.models import (
|
|
29
|
+
ConfigDetail,
|
|
30
|
+
ConfigSummary,
|
|
31
|
+
CreateDraftResponse,
|
|
32
|
+
CursorData,
|
|
33
|
+
DraftTopology,
|
|
34
|
+
DraftValidateResponse,
|
|
35
|
+
DraftValidationErrorItem,
|
|
36
|
+
DraftValidationResult,
|
|
37
|
+
ListNode,
|
|
38
|
+
ListNodesResponse,
|
|
39
|
+
LlmsDoc,
|
|
40
|
+
NodeKind,
|
|
41
|
+
PaginatedList,
|
|
42
|
+
PublishConfigResponse,
|
|
43
|
+
SaveTemplateResponse,
|
|
44
|
+
StateSummary,
|
|
45
|
+
TemplateResponse,
|
|
46
|
+
TFSResponse,
|
|
47
|
+
TopologyNode,
|
|
48
|
+
UpdateDraftResponse,
|
|
49
|
+
compute_config_hash,
|
|
50
|
+
)
|
|
51
|
+
from theseus_kit.oauth import TheseusTokenVerifier, build_token_verifier
|
|
52
|
+
from theseus_kit.resources import set_last_locator
|
|
53
|
+
from theseus_kit.routing import RequestContext
|
|
54
|
+
from theseus_kit.server import create_mcp_server
|
|
55
|
+
from theseus_kit.services import (
|
|
56
|
+
ConfigPublisher,
|
|
57
|
+
ConfigReader,
|
|
58
|
+
DraftCreator,
|
|
59
|
+
DraftEditor,
|
|
60
|
+
DraftValidator,
|
|
61
|
+
LlmsDocReader,
|
|
62
|
+
TemplateSaver,
|
|
63
|
+
)
|
|
64
|
+
from theseus_kit.transport import RobotClient, StaticTokenSource
|
|
65
|
+
|
|
66
|
+
__version__ = "0.1.0"
|
|
67
|
+
|
|
68
|
+
__all__ = [
|
|
69
|
+
"__version__",
|
|
70
|
+
"TheseusSettings",
|
|
71
|
+
"RobotTarget",
|
|
72
|
+
"CredentialConfig",
|
|
73
|
+
"OAuthConfig",
|
|
74
|
+
"UserPatConfig",
|
|
75
|
+
"RequestContext",
|
|
76
|
+
"RobotClient",
|
|
77
|
+
"StaticTokenSource",
|
|
78
|
+
"TheseusError",
|
|
79
|
+
"ConfigError",
|
|
80
|
+
"ConfigLocatorError",
|
|
81
|
+
"DraftConflictError",
|
|
82
|
+
"DraftNotFoundError",
|
|
83
|
+
"LlmsDocError",
|
|
84
|
+
"RoutingConfigError",
|
|
85
|
+
"CredentialError",
|
|
86
|
+
"ScopeOrAudienceError",
|
|
87
|
+
"SubscriptionFrozenError",
|
|
88
|
+
"ExchangeUnavailableError",
|
|
89
|
+
"AuthRejectedError",
|
|
90
|
+
"RobotApiError",
|
|
91
|
+
"RobotValidationError",
|
|
92
|
+
"TheseusTokenVerifier",
|
|
93
|
+
"build_token_verifier",
|
|
94
|
+
"create_mcp_server",
|
|
95
|
+
"ConfigReader",
|
|
96
|
+
"DraftEditor",
|
|
97
|
+
"LlmsDocReader",
|
|
98
|
+
"ConfigSummary",
|
|
99
|
+
"StateSummary",
|
|
100
|
+
"ListNodesResponse",
|
|
101
|
+
"LlmsDoc",
|
|
102
|
+
"ListNode",
|
|
103
|
+
"NodeKind",
|
|
104
|
+
"ConfigDetail",
|
|
105
|
+
"CreateDraftResponse",
|
|
106
|
+
"DraftTopology",
|
|
107
|
+
"DraftValidateResponse",
|
|
108
|
+
"DraftValidationErrorItem",
|
|
109
|
+
"DraftValidationResult",
|
|
110
|
+
"TemplateResponse",
|
|
111
|
+
"TopologyNode",
|
|
112
|
+
"CursorData",
|
|
113
|
+
"TFSResponse",
|
|
114
|
+
"PaginatedList",
|
|
115
|
+
"UpdateDraftResponse",
|
|
116
|
+
"PublishConfigResponse",
|
|
117
|
+
"SaveTemplateResponse",
|
|
118
|
+
"compute_config_hash",
|
|
119
|
+
"ConfigPublisher",
|
|
120
|
+
"DraftCreator",
|
|
121
|
+
"DraftValidator",
|
|
122
|
+
"TemplateSaver",
|
|
123
|
+
"PublishNotConfirmedError",
|
|
124
|
+
"PublishPreCheckError",
|
|
125
|
+
"set_last_locator",
|
|
126
|
+
]
|
theseus_kit/__main__.py
ADDED
theseus_kit/config.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Configuration: robot routing target + credential, loaded from env or a file.
|
|
2
|
+
|
|
3
|
+
Routing metadata is **explicit** (config-injected), never discovered from
|
|
4
|
+
Manager — editing a specific robot inherently requires declaring which one, and
|
|
5
|
+
embedded/internal deployments inject these values externally.
|
|
6
|
+
|
|
7
|
+
Credentials default to ``user_pat`` — the user's personal access token is
|
|
8
|
+
exchanged for a robot-scoped JWT via Manager's token-exchange endpoint.
|
|
9
|
+
All secret fields are pydantic ``SecretStr`` so default ``repr`` / logs never expose
|
|
10
|
+
them.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Annotated, Literal
|
|
17
|
+
|
|
18
|
+
from pydantic import BaseModel, Field, SecretStr, field_validator
|
|
19
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RobotTarget(BaseModel):
|
|
23
|
+
"""One target robot's routing identity + endpoints + TLS.
|
|
24
|
+
|
|
25
|
+
None of these is the token ``audience``: the ``audience`` is a credential
|
|
26
|
+
concern (``robot:{public_id}``) derived from the credential config. The
|
|
27
|
+
``rid`` here is the ``X-TF-RobotId`` value — a different identifier.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
robot_id: str = Field(description="rid → X-TF-RobotId 与集群内路由(≠ public_id)")
|
|
31
|
+
namespace: str = Field(description="租户 K8s Namespace → X-TF-Namespace")
|
|
32
|
+
robot_type: str = Field(description="机器人类型(如 tfrobot / openclaw)→ X-TF-RobotType")
|
|
33
|
+
api_base_url: str = Field(description="机器人 HTTP 入口 https://api.<clusterDomain>")
|
|
34
|
+
manager_base_url: str = Field(description="TFRSManager 公共入口(POST /api/v1/oauth/token 的 host)")
|
|
35
|
+
verify: bool = Field(default=True, description="HTTPS 证书校验")
|
|
36
|
+
ca_bundle: Path | None = Field(default=None, description="自定义 CA bundle(自签 / 本地开发兜底)")
|
|
37
|
+
timeout_s: float = Field(default=30.0, ge=1.0, description="HTTP 超时(秒)")
|
|
38
|
+
|
|
39
|
+
@field_validator("api_base_url", "manager_base_url")
|
|
40
|
+
@classmethod
|
|
41
|
+
def _validate_url_scheme(cls, value: str) -> str:
|
|
42
|
+
if not value.startswith(("http://", "https://")):
|
|
43
|
+
raise ValueError(f"URL must start with http:// or https://; got {value!r}")
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
@field_validator("ca_bundle")
|
|
47
|
+
@classmethod
|
|
48
|
+
def _validate_ca_bundle(cls, value: Path | None) -> Path | None:
|
|
49
|
+
if value is not None and not value.is_file():
|
|
50
|
+
raise ValueError(f"ca_bundle file not found: {value}")
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class UserPatConfig(BaseModel):
|
|
55
|
+
"""User personal access token (direct-user / future-OAuth path).
|
|
56
|
+
|
|
57
|
+
A user PAT can target any of the user's robots, so the target robot's
|
|
58
|
+
``public_id`` (``{orgSlug}:{employeeNo}``) must be named explicitly as the
|
|
59
|
+
token ``audience``.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
kind: Literal["user_pat"] = "user_pat"
|
|
63
|
+
pat: SecretStr = Field(description="用户个人访问令牌(tfp_…)")
|
|
64
|
+
robot_public_id: str = Field(
|
|
65
|
+
description="目标机器人 public_id({orgSlug}:{employeeNo})→ audience robot:{public_id}",
|
|
66
|
+
pattern=r"^[a-z0-9-]+:[a-zA-Z0-9]+$",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class OAuthConfig(BaseModel):
|
|
71
|
+
"""OAuth 2.0 / 2.1 authorization (MCP-standard, no PAT).
|
|
72
|
+
|
|
73
|
+
When no explicit PAT is configured, theseus-kit
|
|
74
|
+
acts as an OAuth Protected Resource (RS): the MCP Client drives the
|
|
75
|
+
authorization-code + PKCE flow against the TFRSManager AS, and theseus-kit
|
|
76
|
+
validates the resulting Bearer token then forwards it directly to the target
|
|
77
|
+
TFRobotServer (no token exchange — TFRobotServer natively accepts OAuth AS
|
|
78
|
+
tokens per §10.1-new of the OAuth design).
|
|
79
|
+
|
|
80
|
+
.. seealso:: :ref:`docs/auth-oauth-design.md` §4, §5.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
kind: Literal["oauth"] = "oauth"
|
|
84
|
+
authorization_server: str = Field(
|
|
85
|
+
description="TFRSManager OAuth AS base URL(用于 PRM 发现 + Bearer 校验的 JWKS 获取)",
|
|
86
|
+
)
|
|
87
|
+
scopes: str = Field(
|
|
88
|
+
default="config:read",
|
|
89
|
+
description="Space-separated scope string(与 PAT 路径默认值一致)",
|
|
90
|
+
)
|
|
91
|
+
client_id: str | None = Field(
|
|
92
|
+
default=None,
|
|
93
|
+
description="预注册 client_id;None 时走 DCR / CIMD(MCP SDK 处理)",
|
|
94
|
+
)
|
|
95
|
+
redirect_uri: str | None = Field(
|
|
96
|
+
default=None,
|
|
97
|
+
description="STDIO 外部回调 URI(Topology B);Topology A(HTTP/MCP Client)不需要",
|
|
98
|
+
)
|
|
99
|
+
resource_server_url: str | None = Field(
|
|
100
|
+
default=None,
|
|
101
|
+
description=(
|
|
102
|
+
"theseus-kit PRM resource URL(Topology A HTTP 入口),"
|
|
103
|
+
"用作 token audience 校验 + AuthSettings.resource_server_url;"
|
|
104
|
+
"None 跳过 audience 校验(宽松模式,仅开发/调试)"
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@field_validator("authorization_server")
|
|
109
|
+
@classmethod
|
|
110
|
+
def _validate_authorization_server_url(cls, value: str) -> str:
|
|
111
|
+
if not value.startswith(("http://", "https://")):
|
|
112
|
+
raise ValueError(f"authorization_server must start with http:// or https://; got {value!r}")
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
@field_validator("redirect_uri")
|
|
116
|
+
@classmethod
|
|
117
|
+
def _validate_redirect_uri_url(cls, value: str | None) -> str | None:
|
|
118
|
+
if value is not None and not value.startswith(("http://", "https://")):
|
|
119
|
+
raise ValueError(f"redirect_uri must start with http:// or https://; got {value!r}")
|
|
120
|
+
return value
|
|
121
|
+
|
|
122
|
+
@field_validator("resource_server_url")
|
|
123
|
+
@classmethod
|
|
124
|
+
def _validate_resource_server_url(cls, value: str | None) -> str | None:
|
|
125
|
+
if value is not None and not value.startswith(("http://", "https://")):
|
|
126
|
+
raise ValueError(f"resource_server_url must start with http:// or https://; got {value!r}")
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
CredentialConfig = Annotated[UserPatConfig | OAuthConfig, Field(discriminator="kind")]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class TheseusSettings(BaseSettings):
|
|
134
|
+
"""theseus-kit settings, loadable from env vars or a ``.env`` file.
|
|
135
|
+
|
|
136
|
+
Env nesting uses ``__``. Examples::
|
|
137
|
+
|
|
138
|
+
THESEUS_ROBOT__ROBOT_ID=robot-1
|
|
139
|
+
THESEUS_ROBOT__NAMESPACE=default
|
|
140
|
+
THESEUS_ROBOT__API_BASE_URL=https://api.example.com
|
|
141
|
+
THESEUS_CREDENTIAL__KIND=user_pat
|
|
142
|
+
THESEUS_CREDENTIAL__PAT=tfp_xxx
|
|
143
|
+
THESEUS_CREDENTIAL__ROBOT_PUBLIC_ID=myorg:12345
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
model_config = SettingsConfigDict(
|
|
147
|
+
env_prefix="theseus_",
|
|
148
|
+
env_nested_delimiter="__",
|
|
149
|
+
env_file=".env",
|
|
150
|
+
env_file_encoding="utf-8",
|
|
151
|
+
extra="ignore",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
robot: RobotTarget
|
|
155
|
+
credential: CredentialConfig
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Build a tfrs-auth ``Credential`` from theseus-kit config.
|
|
2
|
+
|
|
3
|
+
``user_pat`` yields a ``tfrs_auth.PatCredential`` driven by
|
|
4
|
+
``AsyncCachingTokenSource`` (token-exchange + cache + refresh).
|
|
5
|
+
OAuth is handled via ``StaticTokenSource`` — see :mod:`theseus_kit.oauth`.
|
|
6
|
+
theseus-kit never reimplements the exchange / refresh / retry algorithm.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
|
|
13
|
+
from tfrs_auth import PatCredential, Scope, robot_audience, scopes_to_str
|
|
14
|
+
|
|
15
|
+
from .config import CredentialConfig, OAuthConfig, UserPatConfig
|
|
16
|
+
from .errors import ConfigError
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_public_id(public_id: str, *, field_name: str) -> tuple[str, str]:
|
|
20
|
+
"""Parse a ``{orgSlug}:{employeeNo}`` public_id into its two components.
|
|
21
|
+
|
|
22
|
+
The format is contractually defined by Manager's ``publicid`` module and
|
|
23
|
+
mirrored in tfrs-auth's ``contract.public_id()``.
|
|
24
|
+
"""
|
|
25
|
+
if ":" not in public_id:
|
|
26
|
+
raise ConfigError(f"{field_name} must be in public_id format ({{orgSlug}}:{{employeeNo}}), got {public_id!r}")
|
|
27
|
+
org_slug, employee_no = public_id.rsplit(":", 1)
|
|
28
|
+
if not org_slug or not employee_no:
|
|
29
|
+
raise ConfigError(f"{field_name} has empty org_slug or employee_no: {public_id!r}")
|
|
30
|
+
return org_slug, employee_no
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_credential(
|
|
34
|
+
cred: CredentialConfig,
|
|
35
|
+
*,
|
|
36
|
+
scopes: Sequence[Scope | str] = (Scope.CONFIG_READ,),
|
|
37
|
+
) -> PatCredential:
|
|
38
|
+
"""Construct the tfrs-auth credential for the configured source.
|
|
39
|
+
|
|
40
|
+
``user_pat`` exchanges the user's PAT for a robot-scoped JWT via Manager's
|
|
41
|
+
token-exchange endpoint (RFC 8693). The token ``audience`` is
|
|
42
|
+
``robot:{public_id}`` where *public_id* identifies the target robot.
|
|
43
|
+
"""
|
|
44
|
+
if isinstance(cred, UserPatConfig):
|
|
45
|
+
org_slug, employee_no = _parse_public_id(cred.robot_public_id, field_name="robot_public_id")
|
|
46
|
+
return PatCredential(
|
|
47
|
+
pat=cred.pat.get_secret_value(),
|
|
48
|
+
audience=robot_audience(org_slug, employee_no),
|
|
49
|
+
scope=scopes_to_str(scopes),
|
|
50
|
+
)
|
|
51
|
+
if isinstance(cred, OAuthConfig):
|
|
52
|
+
raise ConfigError(
|
|
53
|
+
"OAuth credentials bypass the token exchange pipeline. "
|
|
54
|
+
"The OAuth path uses a static bearer token held by the MCP Client "
|
|
55
|
+
"and injected by RobotClient — no AsyncCachingTokenSource is needed. "
|
|
56
|
+
"Use the OAuth static bearer path instead of build_credential() / build_token_source()."
|
|
57
|
+
)
|
|
58
|
+
# Closed discriminated union — fail explicitly if a new kind is added without
|
|
59
|
+
# wiring it here (decouples runtime validation from mypy type narrowing).
|
|
60
|
+
raise ConfigError(f"unsupported credential kind: {cred!r}") # pyright: ignore[reportUnreachable]
|
theseus_kit/errors.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Typed errors for theseus-kit's auth + routing layer.
|
|
2
|
+
|
|
3
|
+
theseus-kit surfaces tfrs-auth exchange failures and robot-side failures as
|
|
4
|
+
typed exceptions so callers branch on failure mode rather than string-matching.
|
|
5
|
+
All messages are scrubbed of credentials/tokens via :mod:`theseus_kit.redaction`.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from tfrs_auth.errors import (
|
|
11
|
+
InvalidClientError,
|
|
12
|
+
InvalidGrantError,
|
|
13
|
+
InvalidScopeError,
|
|
14
|
+
InvalidTargetError,
|
|
15
|
+
PaymentRequiredError,
|
|
16
|
+
RateLimitedError,
|
|
17
|
+
TemporarilyUnavailableError,
|
|
18
|
+
TfrsAuthError,
|
|
19
|
+
TransportError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .redaction import redact_secrets
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TheseusError(Exception):
|
|
26
|
+
"""Base class for all theseus-kit errors."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConfigError(TheseusError):
|
|
30
|
+
"""Invalid theseus-kit configuration."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RoutingConfigError(ConfigError):
|
|
34
|
+
"""A routing field (namespace / rid / robot_type) is missing or malformed."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CredentialError(TheseusError):
|
|
38
|
+
"""The configured credential was rejected by the token endpoint."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ScopeOrAudienceError(TheseusError):
|
|
42
|
+
"""The requested audience / scope is not grantable for this credential."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ExchangeUnavailableError(TheseusError):
|
|
46
|
+
"""The token endpoint is temporarily unavailable (may be retryable)."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, message: str, *, retryable: bool) -> None:
|
|
49
|
+
self.retryable = retryable
|
|
50
|
+
super().__init__(message)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AuthRejectedError(TheseusError):
|
|
54
|
+
"""The robot rejected the request (401 / 403): credential invalid or scope insufficient."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SubscriptionFrozenError(TheseusError):
|
|
58
|
+
"""The target robot's organization subscription is frozen (HTTP 402)."""
|
|
59
|
+
|
|
60
|
+
def __init__(self, message: str, *, renew_url: str | None) -> None:
|
|
61
|
+
self.renew_url = renew_url
|
|
62
|
+
super().__init__(message)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RobotApiError(TheseusError):
|
|
66
|
+
"""A non-auth robot API failure (4xx / 5xx, or network with no HTTP response)."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, message: str, *, status_code: int) -> None:
|
|
69
|
+
# ``status_code == 0`` means no HTTP response was received (network failure).
|
|
70
|
+
self.status_code = status_code
|
|
71
|
+
super().__init__(message)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ConfigLocatorError(TheseusError):
|
|
75
|
+
"""The supplied locator string is malformed or references an unknown state."""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class LlmsDocError(TheseusError):
|
|
79
|
+
"""The requested llms.txt document path is invalid or forbidden."""
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class DraftNotFoundError(TheseusError):
|
|
83
|
+
"""The requested draft setting_id does not exist (HTTP 404)."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class DraftConflictError(TheseusError):
|
|
87
|
+
"""The draft was modified by another actor since it was read.
|
|
88
|
+
|
|
89
|
+
Carries *current_hash* so the caller can re-read and retry with the
|
|
90
|
+
latest content hash.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self, message: str, *, current_hash: str) -> None:
|
|
94
|
+
self.current_hash = current_hash
|
|
95
|
+
super().__init__(message)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class PublishNotConfirmedError(TheseusError):
|
|
99
|
+
"""The caller must explicitly acknowledge the publish action (acknowledge_publish=True).
|
|
100
|
+
|
|
101
|
+
Publishing a draft configuration is a global, irreversible side-effect. This
|
|
102
|
+
guard forces the caller to confirm it understood the consequences before the
|
|
103
|
+
request is sent upstream.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class PublishPreCheckError(TheseusError):
|
|
108
|
+
"""A pre-publish guard failed — the root hash doesn't match, or the draft
|
|
109
|
+
structure changed since the caller last read it.
|
|
110
|
+
|
|
111
|
+
The caller should re-read the current draft state to understand the
|
|
112
|
+
discrepancy before retrying.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class RobotValidationError(RobotApiError):
|
|
117
|
+
"""The robot rejected the request body as invalid (HTTP 422).
|
|
118
|
+
|
|
119
|
+
Carries *validation_message* from TFRobotServer's ``msg`` field
|
|
120
|
+
(fell back to ``message`` if absent — the 422 handler uses ``msg``
|
|
121
|
+
instead of ``message``).
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, message: str, *, status_code: int = 422, validation_message: str) -> None:
|
|
125
|
+
self.validation_message = validation_message
|
|
126
|
+
super().__init__(message, status_code=status_code)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def map_exchange_error(exc: TfrsAuthError) -> TheseusError:
|
|
130
|
+
"""Map a tfrs-auth exchange/network error to a theseus-kit typed error.
|
|
131
|
+
|
|
132
|
+
Called when ``AsyncCachingTokenSource.token()`` raises. Covers both the
|
|
133
|
+
OAuth error body (``TokenExchangeError`` subclasses) and the network-layer
|
|
134
|
+
``TransportError``. Messages are scrubbed defensively.
|
|
135
|
+
"""
|
|
136
|
+
message = redact_secrets(str(exc))
|
|
137
|
+
if isinstance(exc, PaymentRequiredError):
|
|
138
|
+
return SubscriptionFrozenError(
|
|
139
|
+
_hint(message, "目标机器人所属组织订阅已冻结(402),需续费后重试。"),
|
|
140
|
+
renew_url=exc.renew_url,
|
|
141
|
+
)
|
|
142
|
+
if isinstance(exc, TransportError):
|
|
143
|
+
return ExchangeUnavailableError(
|
|
144
|
+
_hint(message, "换发端点网络不可达(连接 / 超时 / DNS),可稍后重试。"),
|
|
145
|
+
retryable=True,
|
|
146
|
+
)
|
|
147
|
+
if isinstance(exc, (TemporarilyUnavailableError, RateLimitedError)):
|
|
148
|
+
return ExchangeUnavailableError(
|
|
149
|
+
_hint(message, "换发暂不可用(限流 429 / 服务不可用 503),可稍后重试。"),
|
|
150
|
+
retryable=exc.retryable,
|
|
151
|
+
)
|
|
152
|
+
if isinstance(exc, (InvalidGrantError, InvalidClientError)):
|
|
153
|
+
return CredentialError(_hint(message, "凭证被拒:检查 PAT / 机器凭证是否有效、未撤销或未过期。"))
|
|
154
|
+
if isinstance(exc, (InvalidTargetError, InvalidScopeError)):
|
|
155
|
+
return ScopeOrAudienceError(
|
|
156
|
+
_hint(
|
|
157
|
+
message,
|
|
158
|
+
"受众 / Scope 不足:确认 audience=robot:{public_id}、scope(只读=config:read)。",
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
return TheseusError(_hint(message, "令牌换发失败。"))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _hint(message: str, hint: str) -> str:
|
|
165
|
+
body = message.strip()
|
|
166
|
+
return f"{hint}(上游:{body})" if body else hint
|