managed-deepagents 0.6.2.dev5__py3-none-win_amd64.whl → 0.6.2.dev7__py3-none-win_amd64.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.
- managed_deepagents/__init__.py +13 -1
- managed_deepagents/_agent_auth.py +260 -0
- managed_deepagents/_agent_auth_http.py +119 -0
- managed_deepagents/_binaries/mda.exe +0 -0
- managed_deepagents/_connectors/mcp/__init__.py +12 -2
- managed_deepagents/_connectors/mcp/load.py +172 -6
- managed_deepagents/_credential_gate.py +372 -0
- managed_deepagents/_credentials.py +417 -0
- managed_deepagents/_identity_auth.py +1 -1
- managed_deepagents/_identity_runtime.py +310 -7
- managed_deepagents/_managed_middleware.py +10 -9
- managed_deepagents/_managed_tools.py +20 -23
- managed_deepagents/identity/__init__.py +58 -5
- managed_deepagents/runtime.py +31 -4
- {managed_deepagents-0.6.2.dev5.dist-info → managed_deepagents-0.6.2.dev7.dist-info}/METADATA +1 -1
- {managed_deepagents-0.6.2.dev5.dist-info → managed_deepagents-0.6.2.dev7.dist-info}/RECORD +20 -16
- {managed_deepagents-0.6.2.dev5.dist-info → managed_deepagents-0.6.2.dev7.dist-info}/WHEEL +0 -0
- {managed_deepagents-0.6.2.dev5.dist-info → managed_deepagents-0.6.2.dev7.dist-info}/entry_points.txt +0 -0
- {managed_deepagents-0.6.2.dev5.dist-info → managed_deepagents-0.6.2.dev7.dist-info}/licenses/LICENSE +0 -0
- {managed_deepagents-0.6.2.dev5.dist-info → managed_deepagents-0.6.2.dev7.dist-info}/top_level.txt +0 -0
managed_deepagents/__init__.py
CHANGED
|
@@ -9,12 +9,19 @@ This package provides two things:
|
|
|
9
9
|
|
|
10
10
|
Resources use ``define_*`` factories while provider collections use namespaces:
|
|
11
11
|
``define_sandbox()``, ``channels.slack(...)``, ``connectors.mcp(...)``,
|
|
12
|
-
and ``auth.supabase(...)``. Contributing a *new*
|
|
12
|
+
``credentials.get(...)``, and ``auth.supabase(...)``. Contributing a *new*
|
|
13
13
|
primitive is not part of the public surface.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
from managed_deepagents._credentials import (
|
|
19
|
+
CredentialAuthorizationRequiredError,
|
|
20
|
+
CredentialNotFoundError,
|
|
21
|
+
CredentialOwner,
|
|
22
|
+
CredentialStoreError,
|
|
23
|
+
credentials,
|
|
24
|
+
)
|
|
18
25
|
from managed_deepagents._managed_tools import (
|
|
19
26
|
ManagedDeepAgentRuntime,
|
|
20
27
|
)
|
|
@@ -31,6 +38,10 @@ from managed_deepagents.sandbox import SandboxDefinition
|
|
|
31
38
|
from managed_deepagents.schedules import define_schedule
|
|
32
39
|
|
|
33
40
|
__all__ = [
|
|
41
|
+
"CredentialAuthorizationRequiredError",
|
|
42
|
+
"CredentialNotFoundError",
|
|
43
|
+
"CredentialOwner",
|
|
44
|
+
"CredentialStoreError",
|
|
34
45
|
"DeepAgentDefinition",
|
|
35
46
|
"ManagedDeepAgentRuntime",
|
|
36
47
|
"MemoryDefinition",
|
|
@@ -38,6 +49,7 @@ __all__ = [
|
|
|
38
49
|
"auth",
|
|
39
50
|
"channels",
|
|
40
51
|
"connectors",
|
|
52
|
+
"credentials",
|
|
41
53
|
"define_deep_agent",
|
|
42
54
|
"define_identity",
|
|
43
55
|
"define_memory",
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Agent Auth client used by ``credentials.get`` and the pre-run credential gate.
|
|
2
|
+
|
|
3
|
+
Talks to the shipped Agent Auth APIs (credentials + oauth-authorization-sessions),
|
|
4
|
+
not the aspirational ``credentials/mint`` endpoint.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import urllib.parse
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Protocol, TypedDict
|
|
13
|
+
|
|
14
|
+
from managed_deepagents._agent_auth_http import (
|
|
15
|
+
AGENT_AUTH_TIMEOUT_SECONDS,
|
|
16
|
+
CredentialStoreError,
|
|
17
|
+
agent_auth_credential_query,
|
|
18
|
+
agent_auth_headers,
|
|
19
|
+
request_agent_auth_json,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
AGENT_AUTH_PROVIDER_PAGE_SIZE = 100
|
|
23
|
+
AGENT_AUTH_MAX_PROVIDER_PAGES = 20
|
|
24
|
+
|
|
25
|
+
_HTTP_BAD_REQUEST = 400
|
|
26
|
+
_HTTP_NOT_FOUND = 404
|
|
27
|
+
_HTTP_CONFLICT = 409
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"AGENT_AUTH_TIMEOUT_SECONDS",
|
|
31
|
+
"AgentAuth",
|
|
32
|
+
"AgentAuthContext",
|
|
33
|
+
"AgentAuthUnknownProviderError",
|
|
34
|
+
"CredentialAuthorizationRequiredError",
|
|
35
|
+
"PendingAuthorization",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class AgentAuthContext:
|
|
41
|
+
base_url: str
|
|
42
|
+
#: Deployment id, passed to oauth-authorization-sessions as ``agent_id``.
|
|
43
|
+
agent_id: str
|
|
44
|
+
#: Authenticated caller, used as ``owner_id`` for user credentials.
|
|
45
|
+
principal_id: str
|
|
46
|
+
api_key: str | None = None
|
|
47
|
+
workspace_id: str | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class PendingAuthorization(TypedDict):
|
|
51
|
+
"""One credential slug the caller has not granted, with its connect URL."""
|
|
52
|
+
|
|
53
|
+
slug: str
|
|
54
|
+
connect_url: str
|
|
55
|
+
auth_id: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CredentialAuthorizationRequiredError(RuntimeError):
|
|
59
|
+
def __init__(self, slugs: list[str]) -> None:
|
|
60
|
+
super().__init__(
|
|
61
|
+
f"the caller has not connected: {', '.join(slugs)}. Declare the credential "
|
|
62
|
+
"on a connector so the pre-run gate can request it."
|
|
63
|
+
)
|
|
64
|
+
self.slugs = slugs
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AgentAuthUnknownProviderError(RuntimeError):
|
|
68
|
+
def __init__(self, slug: str) -> None:
|
|
69
|
+
super().__init__(
|
|
70
|
+
f'Agent Auth has no OAuth provider registered for slug "{slug}". '
|
|
71
|
+
"Register the provider or fix the credential slug."
|
|
72
|
+
)
|
|
73
|
+
self.slug = slug
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class AgentAuthRequest(Protocol):
|
|
77
|
+
"""HTTP surface bound onto :class:`AgentAuth` (method, path, optional JSON body)."""
|
|
78
|
+
|
|
79
|
+
def __call__(
|
|
80
|
+
self,
|
|
81
|
+
method: str,
|
|
82
|
+
path: str,
|
|
83
|
+
*,
|
|
84
|
+
body: dict[str, object] | None = None,
|
|
85
|
+
) -> tuple[int, dict[str, object]]: ...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class AgentAuth:
|
|
89
|
+
"""Bound Agent Auth HTTP client. Headers, origin, and timeout live on the instance."""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
context: AgentAuthContext,
|
|
94
|
+
*,
|
|
95
|
+
request: AgentAuthRequest | None = None,
|
|
96
|
+
timeout: float = AGENT_AUTH_TIMEOUT_SECONDS,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""Bind this client to a caller.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
context: Caller/run identity and Agent Auth origin.
|
|
102
|
+
request: Testing only. Substitute for the default HTTP transport.
|
|
103
|
+
timeout: Testing only. Override the request budget in seconds.
|
|
104
|
+
"""
|
|
105
|
+
self.context = context
|
|
106
|
+
self._timeout = timeout
|
|
107
|
+
self._request: AgentAuthRequest = request if request is not None else self._urlopen_json
|
|
108
|
+
|
|
109
|
+
def read_user_access_token(self, slug: str) -> str | None:
|
|
110
|
+
"""Read the caller's OAuth access token without starting a session.
|
|
111
|
+
|
|
112
|
+
Reads never mint, so a caller can be probed repeatedly — on every replay
|
|
113
|
+
of an interrupted node, for instance — without leaving authorization
|
|
114
|
+
sessions behind.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
The access token, or ``None`` when the caller must connect.
|
|
118
|
+
"""
|
|
119
|
+
credential_id = self.find_credential_id(
|
|
120
|
+
owner_type="user",
|
|
121
|
+
owner_id=self.context.principal_id,
|
|
122
|
+
kind="oauth2",
|
|
123
|
+
slug=slug,
|
|
124
|
+
)
|
|
125
|
+
if credential_id is None:
|
|
126
|
+
return None
|
|
127
|
+
return self._fetch_access_token(credential_id)
|
|
128
|
+
|
|
129
|
+
def start_authorization_session(self, slug: str) -> PendingAuthorization:
|
|
130
|
+
"""Start an OAuth session for a slug the caller has not granted."""
|
|
131
|
+
provider_id = self.resolve_oauth_provider_id(slug)
|
|
132
|
+
status, body = self._request(
|
|
133
|
+
"POST",
|
|
134
|
+
"/v1/agent-auth/oauth-authorization-sessions",
|
|
135
|
+
body={
|
|
136
|
+
"provider_id": provider_id,
|
|
137
|
+
"principal_id": self.context.principal_id,
|
|
138
|
+
"agent_id": self.context.agent_id,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
if status >= _HTTP_BAD_REQUEST:
|
|
142
|
+
raise CredentialStoreError(
|
|
143
|
+
"Agent Auth create oauth-authorization-session failed", status
|
|
144
|
+
)
|
|
145
|
+
auth_id = str(body.get("id") or "")
|
|
146
|
+
connect_url = str(body.get("verification_url") or "")
|
|
147
|
+
if not auth_id or not connect_url:
|
|
148
|
+
raise CredentialStoreError(
|
|
149
|
+
"Agent Auth oauth-authorization-session missing id or verification_url"
|
|
150
|
+
)
|
|
151
|
+
return {"slug": slug, "connect_url": connect_url, "auth_id": auth_id}
|
|
152
|
+
|
|
153
|
+
def resolve_oauth_provider_id(self, slug: str) -> str:
|
|
154
|
+
"""oauth-providers has no slug filter, so the match is made across pages."""
|
|
155
|
+
cursor: str | None = None
|
|
156
|
+
for _ in range(AGENT_AUTH_MAX_PROVIDER_PAGES):
|
|
157
|
+
query = {"page_size": str(AGENT_AUTH_PROVIDER_PAGE_SIZE)}
|
|
158
|
+
if cursor:
|
|
159
|
+
query["cursor"] = cursor
|
|
160
|
+
status, body = self._request(
|
|
161
|
+
"GET", f"/v1/agent-auth/oauth-providers?{urllib.parse.urlencode(query)}"
|
|
162
|
+
)
|
|
163
|
+
if status >= _HTTP_BAD_REQUEST:
|
|
164
|
+
raise CredentialStoreError("Agent Auth list oauth-providers failed", status)
|
|
165
|
+
raw_items = body.get("items")
|
|
166
|
+
for item in raw_items if isinstance(raw_items, list) else []:
|
|
167
|
+
if isinstance(item, Mapping) and item.get("slug") == slug and item.get("id"):
|
|
168
|
+
return str(item["id"])
|
|
169
|
+
next_cursor = body.get("next_cursor")
|
|
170
|
+
if not isinstance(next_cursor, str) or not next_cursor or next_cursor == cursor:
|
|
171
|
+
raise AgentAuthUnknownProviderError(slug)
|
|
172
|
+
cursor = next_cursor
|
|
173
|
+
raise CredentialStoreError("Agent Auth oauth-provider list returned too many pages")
|
|
174
|
+
|
|
175
|
+
def find_credential_id(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
owner_type: str,
|
|
179
|
+
owner_id: str,
|
|
180
|
+
kind: str,
|
|
181
|
+
slug: str,
|
|
182
|
+
) -> str | None:
|
|
183
|
+
"""Find a credential id by slug.
|
|
184
|
+
|
|
185
|
+
The credential key is ``credential_id``, never ``id``.
|
|
186
|
+
"""
|
|
187
|
+
query = agent_auth_credential_query(
|
|
188
|
+
owner_type=owner_type, owner_id=owner_id, kind=kind, slug=slug
|
|
189
|
+
)
|
|
190
|
+
status, body = self._request("GET", f"/v1/agent-auth/credentials?{query}")
|
|
191
|
+
if status >= _HTTP_BAD_REQUEST:
|
|
192
|
+
raise CredentialStoreError("Agent Auth list credentials failed", status)
|
|
193
|
+
items = body.get("items")
|
|
194
|
+
row = items[0] if isinstance(items, list) and items else None
|
|
195
|
+
credential_id = row.get("credential_id") if isinstance(row, Mapping) else None
|
|
196
|
+
return credential_id if isinstance(credential_id, str) else None
|
|
197
|
+
|
|
198
|
+
def read_secret(self, credential_id: str) -> str | None:
|
|
199
|
+
status, body = self._request(
|
|
200
|
+
"GET",
|
|
201
|
+
f"/v1/agent-auth/credentials/{_path_segment(credential_id)}/secret",
|
|
202
|
+
)
|
|
203
|
+
if status == _HTTP_NOT_FOUND:
|
|
204
|
+
return None
|
|
205
|
+
if status >= _HTTP_BAD_REQUEST:
|
|
206
|
+
raise CredentialStoreError("Agent Auth get secret failed", status)
|
|
207
|
+
secret = body.get("secret")
|
|
208
|
+
return secret if isinstance(secret, str) and secret else None
|
|
209
|
+
|
|
210
|
+
def _fetch_access_token(self, credential_id: str) -> str | None:
|
|
211
|
+
status, body = self._request(
|
|
212
|
+
"GET",
|
|
213
|
+
f"/v1/agent-auth/credentials/{_path_segment(credential_id)}/oauth2",
|
|
214
|
+
)
|
|
215
|
+
if _is_authorization_required(status, body):
|
|
216
|
+
return None
|
|
217
|
+
if status >= _HTTP_BAD_REQUEST:
|
|
218
|
+
raise CredentialStoreError("Agent Auth get oauth2 failed", status)
|
|
219
|
+
oauth2 = body.get("oauth2") if isinstance(body.get("oauth2"), dict) else body
|
|
220
|
+
token = oauth2.get("access_token") if isinstance(oauth2, dict) else None
|
|
221
|
+
return token if isinstance(token, str) and token else None
|
|
222
|
+
|
|
223
|
+
def _urlopen_json(
|
|
224
|
+
self,
|
|
225
|
+
method: str,
|
|
226
|
+
path: str,
|
|
227
|
+
*,
|
|
228
|
+
body: dict[str, object] | None = None,
|
|
229
|
+
) -> tuple[int, dict[str, object]]:
|
|
230
|
+
if not self.context.base_url.startswith(("http://", "https://")):
|
|
231
|
+
raise ValueError("Agent Auth URL must be an http(s) URL")
|
|
232
|
+
return request_agent_auth_json(
|
|
233
|
+
method,
|
|
234
|
+
f"{self.context.base_url}{path}",
|
|
235
|
+
body=body,
|
|
236
|
+
headers=self._headers(),
|
|
237
|
+
timeout=self._timeout,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def _headers(self) -> dict[str, str]:
|
|
241
|
+
return agent_auth_headers(
|
|
242
|
+
api_key=self.context.api_key,
|
|
243
|
+
workspace_id=self.context.workspace_id,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _path_segment(value: str) -> str:
|
|
248
|
+
return urllib.parse.quote(value, safe="")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _is_authorization_required(status: int, body: dict[str, object]) -> bool:
|
|
252
|
+
"""Agent Auth answers 409 when a stored grant has lapsed and needs re-auth."""
|
|
253
|
+
if status != _HTTP_CONFLICT:
|
|
254
|
+
return False
|
|
255
|
+
detail = str(body.get("detail") or body.get("title") or body.get("error") or "").lower()
|
|
256
|
+
return (
|
|
257
|
+
body.get("error") == "authorization_required"
|
|
258
|
+
or "authorization is required" in detail
|
|
259
|
+
or "oauth authorization" in detail
|
|
260
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Shared Agent Auth HTTP transport and credential-list paging.
|
|
2
|
+
|
|
3
|
+
Used by ``credentials.get`` and the credential gate so both talk to the same
|
|
4
|
+
API with the same headers, redirect policy, and page walker.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from http.client import HTTPMessage
|
|
18
|
+
from typing import IO
|
|
19
|
+
|
|
20
|
+
AGENT_AUTH_TIMEOUT_SECONDS = 10.0
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CredentialStoreError(RuntimeError):
|
|
24
|
+
"""Agent Auth could not answer.
|
|
25
|
+
|
|
26
|
+
Callers pass a fixed message: a response body can quote secret material, so
|
|
27
|
+
it never reaches this error, the logs, or the traceback chain.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, message: str, status: int | None = None) -> None:
|
|
31
|
+
self.status = status
|
|
32
|
+
super().__init__(message if status is None else f"{message} (status {status})")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
"AGENT_AUTH_TIMEOUT_SECONDS",
|
|
37
|
+
"CredentialStoreError",
|
|
38
|
+
"agent_auth_credential_query",
|
|
39
|
+
"agent_auth_headers",
|
|
40
|
+
"request_agent_auth_json",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def agent_auth_credential_query(
|
|
45
|
+
*,
|
|
46
|
+
owner_type: str,
|
|
47
|
+
owner_id: str,
|
|
48
|
+
kind: str,
|
|
49
|
+
slug: str,
|
|
50
|
+
) -> str:
|
|
51
|
+
"""Query string that addresses one credential by its natural key."""
|
|
52
|
+
return urllib.parse.urlencode(
|
|
53
|
+
{"owner_type": owner_type, "owner_id": owner_id, "kind": kind, "slug": slug}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def agent_auth_headers(
|
|
58
|
+
*,
|
|
59
|
+
api_key: str | None = None,
|
|
60
|
+
workspace_id: str | None = None,
|
|
61
|
+
) -> dict[str, str]:
|
|
62
|
+
headers = {
|
|
63
|
+
"accept": "application/json",
|
|
64
|
+
"content-type": "application/json",
|
|
65
|
+
"x-langsmith-product": "mda",
|
|
66
|
+
}
|
|
67
|
+
resolved_api_key = (api_key or "").strip()
|
|
68
|
+
if resolved_api_key:
|
|
69
|
+
headers["x-api-key"] = resolved_api_key
|
|
70
|
+
resolved_workspace = (workspace_id or "").strip()
|
|
71
|
+
if resolved_workspace:
|
|
72
|
+
headers["x-tenant-id"] = resolved_workspace
|
|
73
|
+
return headers
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _decode_json_object(raw: bytes) -> dict[str, object]:
|
|
77
|
+
try:
|
|
78
|
+
payload = json.loads(raw.decode("utf-8") or "{}")
|
|
79
|
+
except (UnicodeDecodeError, json.JSONDecodeError, ValueError):
|
|
80
|
+
return {}
|
|
81
|
+
return payload if isinstance(payload, dict) else {}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _NoRedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
85
|
+
def redirect_request(
|
|
86
|
+
self,
|
|
87
|
+
req: urllib.request.Request,
|
|
88
|
+
fp: IO[bytes],
|
|
89
|
+
code: int,
|
|
90
|
+
msg: str,
|
|
91
|
+
headers: HTTPMessage,
|
|
92
|
+
newurl: str,
|
|
93
|
+
) -> urllib.request.Request | None:
|
|
94
|
+
del req, fp, code, msg, headers, newurl
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def request_agent_auth_json(
|
|
99
|
+
method: str,
|
|
100
|
+
url: str,
|
|
101
|
+
*,
|
|
102
|
+
body: Mapping[str, object] | None = None,
|
|
103
|
+
headers: Mapping[str, str],
|
|
104
|
+
timeout: float,
|
|
105
|
+
) -> tuple[int, dict[str, object]]:
|
|
106
|
+
"""Issue one Agent Auth HTTP call: no redirects, bounded timeout, JSON body."""
|
|
107
|
+
data = None if body is None else json.dumps(dict(body)).encode("utf-8")
|
|
108
|
+
request = urllib.request.Request( # noqa: S310
|
|
109
|
+
url,
|
|
110
|
+
data=data,
|
|
111
|
+
headers=dict(headers),
|
|
112
|
+
method=method,
|
|
113
|
+
)
|
|
114
|
+
try:
|
|
115
|
+
opener = urllib.request.build_opener(_NoRedirectHandler())
|
|
116
|
+
with opener.open(request, timeout=timeout) as response:
|
|
117
|
+
return int(response.status), _decode_json_object(response.read())
|
|
118
|
+
except urllib.error.HTTPError as exc:
|
|
119
|
+
return int(exc.code), _decode_json_object(exc.read())
|
|
Binary file
|
|
@@ -9,7 +9,7 @@ name, before any managed ``{server}__`` prefix is applied.
|
|
|
9
9
|
Example::
|
|
10
10
|
|
|
11
11
|
# connectors/mcp.py
|
|
12
|
-
from managed_deepagents import connectors
|
|
12
|
+
from managed_deepagents import connectors, credentials
|
|
13
13
|
|
|
14
14
|
connector = connectors.mcp(
|
|
15
15
|
use_standard_content_blocks=True,
|
|
@@ -21,6 +21,11 @@ Example::
|
|
|
21
21
|
"url": "https://docs.example.com/mcp",
|
|
22
22
|
"include_tools": ["search", "fetch"],
|
|
23
23
|
},
|
|
24
|
+
"notion": {
|
|
25
|
+
"transport": "http",
|
|
26
|
+
"url": "https://mcp.notion.com/mcp",
|
|
27
|
+
"credential": credentials.get("notion", {"type": "user"}),
|
|
28
|
+
},
|
|
24
29
|
},
|
|
25
30
|
)
|
|
26
31
|
"""
|
|
@@ -42,6 +47,7 @@ from pydantic import (
|
|
|
42
47
|
|
|
43
48
|
from managed_deepagents._connector import Connector
|
|
44
49
|
from managed_deepagents._connectors.mcp.load import load_managed_mcp_tools
|
|
50
|
+
from managed_deepagents._credentials import CredentialReference # noqa: TC001
|
|
45
51
|
from managed_deepagents._extension import define_extension
|
|
46
52
|
|
|
47
53
|
|
|
@@ -53,6 +59,9 @@ class McpToolSelectionConfig(TypedDict, total=False):
|
|
|
53
59
|
class RemoteMcpServerBaseConfig(McpToolSelectionConfig, total=False):
|
|
54
60
|
url: str
|
|
55
61
|
headers: dict[str, str]
|
|
62
|
+
#: Bearer credential: an agent owner is a deployment-wide secret read once
|
|
63
|
+
#: at load, a user owner is the caller's OAuth grant loaded after the gate.
|
|
64
|
+
credential: CredentialReference
|
|
56
65
|
#: Default timeout in seconds for tools loaded from this server.
|
|
57
66
|
default_tool_timeout: float
|
|
58
67
|
|
|
@@ -142,7 +151,7 @@ class _RemoteMcpServer(BaseModel):
|
|
|
142
151
|
``default_tool_timeout``) are stripped before the client is constructed.
|
|
143
152
|
"""
|
|
144
153
|
|
|
145
|
-
model_config = ConfigDict(extra="allow", validate_default=True)
|
|
154
|
+
model_config = ConfigDict(extra="allow", validate_default=True, arbitrary_types_allowed=True)
|
|
146
155
|
|
|
147
156
|
# Both default to a value the validators below reject, so a missing field
|
|
148
157
|
# reports the same message a malformed one does — as it does in Zod.
|
|
@@ -154,6 +163,7 @@ class _RemoteMcpServer(BaseModel):
|
|
|
154
163
|
default_tool_timeout: float | None = None
|
|
155
164
|
include_tools: list[_ToolName] | None = None
|
|
156
165
|
exclude_tools: list[_ToolName] | None = None
|
|
166
|
+
credential: CredentialReference | None = None
|
|
157
167
|
|
|
158
168
|
@field_validator("default_tool_timeout")
|
|
159
169
|
@classmethod
|