managed-deepagents 0.6.2.dev30__py3-none-win_amd64.whl → 0.6.2.dev32__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 +8 -8
- managed_deepagents/_agent_auth.py +184 -16
- managed_deepagents/_agent_auth_http.py +21 -5
- managed_deepagents/_binaries/mda.exe +0 -0
- managed_deepagents/_channels/manifest.py +2 -0
- managed_deepagents/_channels/trigger/http.py +1 -1
- managed_deepagents/{_credentials.py → _connections.py} +171 -146
- managed_deepagents/_connectors/mcp/__init__.py +49 -20
- managed_deepagents/_connectors/mcp/load.py +50 -43
- managed_deepagents/_credential_gate.py +86 -89
- {managed_deepagents-0.6.2.dev30.dist-info → managed_deepagents-0.6.2.dev32.dist-info}/METADATA +13 -10
- {managed_deepagents-0.6.2.dev30.dist-info → managed_deepagents-0.6.2.dev32.dist-info}/RECORD +16 -16
- {managed_deepagents-0.6.2.dev30.dist-info → managed_deepagents-0.6.2.dev32.dist-info}/WHEEL +0 -0
- {managed_deepagents-0.6.2.dev30.dist-info → managed_deepagents-0.6.2.dev32.dist-info}/entry_points.txt +0 -0
- {managed_deepagents-0.6.2.dev30.dist-info → managed_deepagents-0.6.2.dev32.dist-info}/licenses/LICENSE +0 -0
- {managed_deepagents-0.6.2.dev30.dist-info → managed_deepagents-0.6.2.dev32.dist-info}/top_level.txt +0 -0
managed_deepagents/__init__.py
CHANGED
|
@@ -9,18 +9,18 @@ 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
|
-
``
|
|
12
|
+
``connections.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.
|
|
18
|
+
from managed_deepagents._connections import (
|
|
19
|
+
ConnectionNotFoundError,
|
|
19
20
|
CredentialAuthorizationRequiredError,
|
|
20
|
-
CredentialNotFoundError,
|
|
21
|
-
CredentialOwner,
|
|
22
21
|
CredentialStoreError,
|
|
23
|
-
|
|
22
|
+
InvalidAgentAuthEndpointError,
|
|
23
|
+
connections,
|
|
24
24
|
)
|
|
25
25
|
from managed_deepagents._managed_tools import (
|
|
26
26
|
ManagedDeepAgentRuntime,
|
|
@@ -38,19 +38,19 @@ from managed_deepagents.sandbox import SandboxDefinition
|
|
|
38
38
|
from managed_deepagents.schedules import define_schedule
|
|
39
39
|
|
|
40
40
|
__all__ = [
|
|
41
|
+
"ConnectionNotFoundError",
|
|
41
42
|
"CredentialAuthorizationRequiredError",
|
|
42
|
-
"CredentialNotFoundError",
|
|
43
|
-
"CredentialOwner",
|
|
44
43
|
"CredentialStoreError",
|
|
45
44
|
"DeepAgentDefinition",
|
|
45
|
+
"InvalidAgentAuthEndpointError",
|
|
46
46
|
"ManagedDeepAgentRuntime",
|
|
47
47
|
"ManagedServerInfo",
|
|
48
48
|
"MemoryDefinition",
|
|
49
49
|
"SandboxDefinition",
|
|
50
50
|
"auth",
|
|
51
51
|
"channels",
|
|
52
|
+
"connections",
|
|
52
53
|
"connectors",
|
|
53
|
-
"credentials",
|
|
54
54
|
"define_deep_agent",
|
|
55
55
|
"define_identity",
|
|
56
56
|
"define_memory",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Agent Auth client used by ``
|
|
1
|
+
"""Agent Auth client used by ``connections.get`` and the pre-run credential gate.
|
|
2
2
|
|
|
3
3
|
Talks to the shipped Agent Auth APIs (credentials + oauth-authorization-sessions),
|
|
4
4
|
not the aspirational ``credentials/mint`` endpoint.
|
|
@@ -33,6 +33,7 @@ __all__ = [
|
|
|
33
33
|
"AgentAuthUnknownProviderError",
|
|
34
34
|
"CredentialAuthorizationRequiredError",
|
|
35
35
|
"PendingAuthorization",
|
|
36
|
+
"WorkspaceConnection",
|
|
36
37
|
]
|
|
37
38
|
|
|
38
39
|
|
|
@@ -45,20 +46,29 @@ class AgentAuthContext:
|
|
|
45
46
|
principal_id: str
|
|
46
47
|
api_key: str | None = None
|
|
47
48
|
workspace_id: str | None = None
|
|
49
|
+
#: Agent Builder service JWT for workspace-owned credential material.
|
|
50
|
+
service_key: str | None = None
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
class PendingAuthorization(TypedDict):
|
|
51
|
-
"""One
|
|
54
|
+
"""One connection slug the caller has not granted, with its connect URL."""
|
|
52
55
|
|
|
53
56
|
slug: str
|
|
54
57
|
connect_url: str
|
|
55
58
|
auth_id: str
|
|
56
59
|
|
|
57
60
|
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class WorkspaceConnection:
|
|
63
|
+
connection_id: str
|
|
64
|
+
kind: str
|
|
65
|
+
owner_policy: str
|
|
66
|
+
|
|
67
|
+
|
|
58
68
|
class CredentialAuthorizationRequiredError(RuntimeError):
|
|
59
69
|
def __init__(self, slugs: list[str]) -> None:
|
|
60
70
|
super().__init__(
|
|
61
|
-
f"the caller has not connected: {', '.join(slugs)}. Declare the
|
|
71
|
+
f"the caller has not connected: {', '.join(slugs)}. Declare the connection "
|
|
62
72
|
"on a connector so the pre-run gate can request it."
|
|
63
73
|
)
|
|
64
74
|
self.slugs = slugs
|
|
@@ -106,6 +116,124 @@ class AgentAuth:
|
|
|
106
116
|
self._timeout = timeout
|
|
107
117
|
self._request: AgentAuthRequest = request if request is not None else self._urlopen_json
|
|
108
118
|
|
|
119
|
+
def find_workspace_connection(self, slug: str) -> WorkspaceConnection | None:
|
|
120
|
+
"""List workspace connections and return the row for ``slug``, if any."""
|
|
121
|
+
if not (self.context.workspace_id or "").strip():
|
|
122
|
+
raise CredentialStoreError(
|
|
123
|
+
"connections are not configured: LANGSMITH_WORKSPACE_ID is not set"
|
|
124
|
+
)
|
|
125
|
+
cursor: str | None = None
|
|
126
|
+
for _ in range(AGENT_AUTH_MAX_PROVIDER_PAGES):
|
|
127
|
+
query: dict[str, str] = {
|
|
128
|
+
"page_size": str(AGENT_AUTH_PROVIDER_PAGE_SIZE),
|
|
129
|
+
}
|
|
130
|
+
if cursor:
|
|
131
|
+
query["cursor"] = cursor
|
|
132
|
+
status, body = self._request(
|
|
133
|
+
"GET",
|
|
134
|
+
f"/v1/agent-auth/connections?{urllib.parse.urlencode(query)}",
|
|
135
|
+
)
|
|
136
|
+
if status >= _HTTP_BAD_REQUEST:
|
|
137
|
+
raise CredentialStoreError("Agent Auth list connections failed", status)
|
|
138
|
+
raw_items = body.get("items")
|
|
139
|
+
items = raw_items if isinstance(raw_items, list) else []
|
|
140
|
+
for item in items:
|
|
141
|
+
if not isinstance(item, Mapping) or item.get("slug") != slug:
|
|
142
|
+
continue
|
|
143
|
+
connection_id = (
|
|
144
|
+
item.get("id") or item.get("connection_id") or item.get("credential_id")
|
|
145
|
+
)
|
|
146
|
+
kind = _connection_credential_kind(item)
|
|
147
|
+
owner_policy = item.get("owner_policy")
|
|
148
|
+
if not isinstance(connection_id, str) or not connection_id:
|
|
149
|
+
raise CredentialStoreError("Agent Auth connection row missing id or kind")
|
|
150
|
+
if not isinstance(kind, str) or not kind:
|
|
151
|
+
raise CredentialStoreError("Agent Auth connection row missing id or kind")
|
|
152
|
+
return WorkspaceConnection(
|
|
153
|
+
connection_id=connection_id,
|
|
154
|
+
kind=kind,
|
|
155
|
+
owner_policy=str(owner_policy) if isinstance(owner_policy, str) else "",
|
|
156
|
+
)
|
|
157
|
+
next_cursor = body.get("next_cursor")
|
|
158
|
+
if not isinstance(next_cursor, str) or not next_cursor or next_cursor == cursor:
|
|
159
|
+
return None
|
|
160
|
+
cursor = next_cursor
|
|
161
|
+
raise CredentialStoreError("Agent Auth connection list returned too many pages")
|
|
162
|
+
|
|
163
|
+
def find_workspace_credential_id(self, slug: str) -> str | None:
|
|
164
|
+
"""Workspace credential id for an agent-owned opaque secret grant."""
|
|
165
|
+
connection = self.find_workspace_connection(slug)
|
|
166
|
+
if connection is None:
|
|
167
|
+
return None
|
|
168
|
+
return self.find_secret_credential_id(connection, slug)
|
|
169
|
+
|
|
170
|
+
def find_secret_credential_id(self, connection: WorkspaceConnection, slug: str) -> str | None:
|
|
171
|
+
"""Resolve the credential id for a secret connection row.
|
|
172
|
+
|
|
173
|
+
With an Agent Builder service key, prefer GET-for-owner / workspace list.
|
|
174
|
+
Personal API keys may only list agent-owned credentials.
|
|
175
|
+
"""
|
|
176
|
+
if connection.kind != "secret":
|
|
177
|
+
return None
|
|
178
|
+
workspace_id = (self.context.workspace_id or "").strip()
|
|
179
|
+
if not workspace_id:
|
|
180
|
+
raise CredentialStoreError(
|
|
181
|
+
"connections are not configured: LANGSMITH_WORKSPACE_ID is not set"
|
|
182
|
+
)
|
|
183
|
+
if (self.context.service_key or "").strip():
|
|
184
|
+
from_owner = self._credential_id_for_owner(
|
|
185
|
+
connection.connection_id, "workspace", workspace_id
|
|
186
|
+
)
|
|
187
|
+
if from_owner is not None:
|
|
188
|
+
return from_owner
|
|
189
|
+
return self.find_credential_id(
|
|
190
|
+
owner_type="workspace",
|
|
191
|
+
owner_id=workspace_id,
|
|
192
|
+
kind="secret",
|
|
193
|
+
slug=slug,
|
|
194
|
+
)
|
|
195
|
+
return self.find_credential_id(
|
|
196
|
+
owner_type="agent",
|
|
197
|
+
owner_id=self.context.agent_id,
|
|
198
|
+
kind="secret",
|
|
199
|
+
slug=slug,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def _credential_id_for_owner(
|
|
203
|
+
self, connection_id: str, owner_type: str, owner_id: str
|
|
204
|
+
) -> str | None:
|
|
205
|
+
query = urllib.parse.urlencode(
|
|
206
|
+
{
|
|
207
|
+
"credential_owner_type": owner_type,
|
|
208
|
+
"credential_owner_id": owner_id,
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
status, body = self._request(
|
|
212
|
+
"GET",
|
|
213
|
+
f"/v1/agent-auth/connections/{_path_segment(connection_id)}?{query}",
|
|
214
|
+
)
|
|
215
|
+
if status == _HTTP_NOT_FOUND:
|
|
216
|
+
return None
|
|
217
|
+
if status >= _HTTP_BAD_REQUEST:
|
|
218
|
+
raise CredentialStoreError("Agent Auth get connection for owner failed", status)
|
|
219
|
+
return _connection_credential_id(body)
|
|
220
|
+
|
|
221
|
+
def read_user_secret(self, slug: str) -> str | None:
|
|
222
|
+
"""Read a caller-owned opaque secret grant for a workspace connection slug."""
|
|
223
|
+
credential_id = self.find_credential_id(
|
|
224
|
+
owner_type="user",
|
|
225
|
+
owner_id=self.context.principal_id,
|
|
226
|
+
kind="secret",
|
|
227
|
+
slug=slug,
|
|
228
|
+
)
|
|
229
|
+
if credential_id is None:
|
|
230
|
+
return None
|
|
231
|
+
return self.read_secret(credential_id)
|
|
232
|
+
|
|
233
|
+
def read_connection_access_token(self, connection_id: str) -> str | None:
|
|
234
|
+
"""Read an OAuth access token stored on a workspace connection row."""
|
|
235
|
+
return self._fetch_access_token(connection_id)
|
|
236
|
+
|
|
109
237
|
def read_user_access_token(self, slug: str) -> str | None:
|
|
110
238
|
"""Read the caller's OAuth access token without starting a session.
|
|
111
239
|
|
|
@@ -128,12 +256,16 @@ class AgentAuth:
|
|
|
128
256
|
|
|
129
257
|
def start_authorization_session(self, slug: str) -> PendingAuthorization:
|
|
130
258
|
"""Start an OAuth session for a slug the caller has not granted."""
|
|
131
|
-
|
|
259
|
+
connection = self.find_workspace_connection(slug)
|
|
260
|
+
if connection is None:
|
|
261
|
+
raise CredentialStoreError(f"workspace connection {slug!r} is not configured")
|
|
262
|
+
if connection.kind != "oauth2":
|
|
263
|
+
raise CredentialStoreError(f"connection {slug!r} is not an OAuth connection")
|
|
132
264
|
status, body = self._request(
|
|
133
265
|
"POST",
|
|
134
266
|
"/v1/agent-auth/oauth-authorization-sessions",
|
|
135
267
|
body={
|
|
136
|
-
"
|
|
268
|
+
"connection_id": connection.connection_id,
|
|
137
269
|
"principal_id": self.context.principal_id,
|
|
138
270
|
"agent_id": self.context.agent_id,
|
|
139
271
|
},
|
|
@@ -182,18 +314,35 @@ class AgentAuth:
|
|
|
182
314
|
) -> str | None:
|
|
183
315
|
"""Find a credential id by slug.
|
|
184
316
|
|
|
185
|
-
The
|
|
317
|
+
The shipped credentials list filters by owner and kind only, so the
|
|
318
|
+
slug match is made client-side across pages. The credential key is
|
|
319
|
+
``credential_id``, never ``id``.
|
|
186
320
|
"""
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
321
|
+
cursor: str | None = None
|
|
322
|
+
for _ in range(AGENT_AUTH_MAX_PROVIDER_PAGES):
|
|
323
|
+
query = agent_auth_credential_query(
|
|
324
|
+
owner_type=owner_type,
|
|
325
|
+
owner_id=owner_id,
|
|
326
|
+
kind=kind,
|
|
327
|
+
page_size=AGENT_AUTH_PROVIDER_PAGE_SIZE,
|
|
328
|
+
cursor=cursor,
|
|
329
|
+
)
|
|
330
|
+
status, body = self._request("GET", f"/v1/agent-auth/credentials?{query}")
|
|
331
|
+
if status >= _HTTP_BAD_REQUEST:
|
|
332
|
+
raise CredentialStoreError("Agent Auth list credentials failed", status)
|
|
333
|
+
raw_items = body.get("items")
|
|
334
|
+
items = raw_items if isinstance(raw_items, list) else []
|
|
335
|
+
for item in items:
|
|
336
|
+
if not isinstance(item, Mapping) or item.get("slug") != slug:
|
|
337
|
+
continue
|
|
338
|
+
credential_id = item.get("credential_id")
|
|
339
|
+
if isinstance(credential_id, str) and credential_id:
|
|
340
|
+
return credential_id
|
|
341
|
+
next_cursor = body.get("next_cursor")
|
|
342
|
+
if not isinstance(next_cursor, str) or not next_cursor or next_cursor == cursor:
|
|
343
|
+
return None
|
|
344
|
+
cursor = next_cursor
|
|
345
|
+
raise CredentialStoreError("Agent Auth credential list returned too many pages")
|
|
197
346
|
|
|
198
347
|
def read_secret(self, credential_id: str) -> str | None:
|
|
199
348
|
status, body = self._request(
|
|
@@ -241,6 +390,7 @@ class AgentAuth:
|
|
|
241
390
|
return agent_auth_headers(
|
|
242
391
|
api_key=self.context.api_key,
|
|
243
392
|
workspace_id=self.context.workspace_id,
|
|
393
|
+
service_key=self.context.service_key,
|
|
244
394
|
)
|
|
245
395
|
|
|
246
396
|
|
|
@@ -248,6 +398,24 @@ def _path_segment(value: str) -> str:
|
|
|
248
398
|
return urllib.parse.quote(value, safe="")
|
|
249
399
|
|
|
250
400
|
|
|
401
|
+
def _connection_credential_kind(item: Mapping[str, object]) -> str:
|
|
402
|
+
credential = item.get("credential")
|
|
403
|
+
if isinstance(credential, Mapping):
|
|
404
|
+
kind = credential.get("kind")
|
|
405
|
+
if isinstance(kind, str) and kind:
|
|
406
|
+
return kind
|
|
407
|
+
kind = item.get("kind")
|
|
408
|
+
return kind if isinstance(kind, str) else ""
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _connection_credential_id(body: Mapping[str, object]) -> str | None:
|
|
412
|
+
credential = body.get("credential")
|
|
413
|
+
if not isinstance(credential, Mapping):
|
|
414
|
+
return None
|
|
415
|
+
credential_id = credential.get("credential_id")
|
|
416
|
+
return credential_id if isinstance(credential_id, str) and credential_id else None
|
|
417
|
+
|
|
418
|
+
|
|
251
419
|
def _is_authorization_required(status: int, body: dict[str, object]) -> bool:
|
|
252
420
|
"""Agent Auth answers 409 when a stored grant has lapsed and needs re-auth."""
|
|
253
421
|
if status != _HTTP_CONFLICT:
|
|
@@ -46,24 +46,40 @@ def agent_auth_credential_query(
|
|
|
46
46
|
owner_type: str,
|
|
47
47
|
owner_id: str,
|
|
48
48
|
kind: str,
|
|
49
|
-
|
|
49
|
+
page_size: int | None = None,
|
|
50
|
+
cursor: str | None = None,
|
|
50
51
|
) -> str:
|
|
51
|
-
"""Query string
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
"""Query string for listing credentials filtered by owner and kind.
|
|
53
|
+
|
|
54
|
+
The shipped list endpoint does not filter by slug; callers match ``slug``
|
|
55
|
+
client-side across pages.
|
|
56
|
+
"""
|
|
57
|
+
params: dict[str, str] = {
|
|
58
|
+
"owner_type": owner_type,
|
|
59
|
+
"owner_id": owner_id,
|
|
60
|
+
"kind": kind,
|
|
61
|
+
}
|
|
62
|
+
if page_size is not None:
|
|
63
|
+
params["page_size"] = str(page_size)
|
|
64
|
+
if cursor:
|
|
65
|
+
params["cursor"] = cursor
|
|
66
|
+
return urllib.parse.urlencode(params)
|
|
55
67
|
|
|
56
68
|
|
|
57
69
|
def agent_auth_headers(
|
|
58
70
|
*,
|
|
59
71
|
api_key: str | None = None,
|
|
60
72
|
workspace_id: str | None = None,
|
|
73
|
+
service_key: str | None = None,
|
|
61
74
|
) -> dict[str, str]:
|
|
62
75
|
headers = {
|
|
63
76
|
"accept": "application/json",
|
|
64
77
|
"content-type": "application/json",
|
|
65
78
|
"x-langsmith-product": "mda",
|
|
66
79
|
}
|
|
80
|
+
resolved_service_key = (service_key or "").strip()
|
|
81
|
+
if resolved_service_key:
|
|
82
|
+
headers["x-service-key"] = resolved_service_key
|
|
67
83
|
resolved_api_key = (api_key or "").strip()
|
|
68
84
|
if resolved_api_key:
|
|
69
85
|
headers["x-api-key"] = resolved_api_key
|
|
Binary file
|
|
@@ -17,6 +17,8 @@ JsonObject = dict[str, JsonValue]
|
|
|
17
17
|
class _OptionalChannelManifest(TypedDict, total=False):
|
|
18
18
|
transport: Literal["trigger_server"]
|
|
19
19
|
userCredentials: list[str]
|
|
20
|
+
userConnections: list[str]
|
|
21
|
+
agentConnections: list[str]
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class ChannelManifest(_OptionalChannelManifest):
|
|
@@ -250,7 +250,7 @@ async def _observe_accepted_run(
|
|
|
250
250
|
event["channel_id"],
|
|
251
251
|
{
|
|
252
252
|
"type": "post",
|
|
253
|
-
"action_id":
|
|
253
|
+
"action_id": event["event_id"],
|
|
254
254
|
"address": event["address"],
|
|
255
255
|
"source_thread_key": event["source_thread_key"],
|
|
256
256
|
"message": {"text": text},
|