managed-deepagents 0.6.2.dev51__py3-none-win_amd64.whl → 0.6.2.dev52__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.
Files changed (26) hide show
  1. managed_deepagents/__init__.py +1 -3
  2. managed_deepagents/_agent_auth.py +127 -43
  3. managed_deepagents/_agent_auth_http.py +65 -9
  4. managed_deepagents/_binaries/mda.exe +0 -0
  5. managed_deepagents/_channels/trigger/http.py +141 -9
  6. managed_deepagents/_channels/trigger/identity_context.py +214 -0
  7. managed_deepagents/_channels/trigger/types.py +14 -2
  8. managed_deepagents/_connections.py +133 -49
  9. managed_deepagents/_connectors/__init__.py +3 -2
  10. managed_deepagents/_connectors/mcp/__init__.py +20 -51
  11. managed_deepagents/_connectors/mcp/load.py +5 -5
  12. managed_deepagents/_connectors/tool_selection.py +1 -1
  13. managed_deepagents/_credential_gate.py +5 -5
  14. managed_deepagents/_extension.py +3 -3
  15. managed_deepagents/_identity_auth.py +69 -8
  16. managed_deepagents/_identity_runtime.py +17 -0
  17. managed_deepagents/_ingress.py +48 -14
  18. managed_deepagents/_validated_token.py +35 -0
  19. managed_deepagents/identity/__init__.py +10 -0
  20. managed_deepagents/namespaces.py +14 -24
  21. {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/METADATA +22 -18
  22. {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/RECORD +26 -25
  23. {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/WHEEL +0 -0
  24. {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/entry_points.txt +0 -0
  25. {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/licenses/LICENSE +0 -0
  26. {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/top_level.txt +0 -0
@@ -8,7 +8,7 @@ This package provides two things:
8
8
  ``mda`` console script.
9
9
 
10
10
  Resources use ``define_*`` factories while provider collections use namespaces:
11
- ``define_sandbox()``, ``define_mcp()``, ``channels.slack(...)``,
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
  """
@@ -32,7 +32,6 @@ from managed_deepagents.namespaces import (
32
32
  auth,
33
33
  channels,
34
34
  connectors,
35
- define_mcp,
36
35
  define_sandbox,
37
36
  )
38
37
  from managed_deepagents.sandbox import SandboxDefinition
@@ -54,7 +53,6 @@ __all__ = [
54
53
  "connectors",
55
54
  "define_deep_agent",
56
55
  "define_identity",
57
- "define_mcp",
58
56
  "define_memory",
59
57
  "define_sandbox",
60
58
  "define_schedule",
@@ -16,6 +16,7 @@ from managed_deepagents._agent_auth_http import (
16
16
  CredentialStoreError,
17
17
  agent_auth_credential_query,
18
18
  agent_auth_headers,
19
+ missing_workspace_connection_error,
19
20
  request_agent_auth_json,
20
21
  )
21
22
 
@@ -23,6 +24,7 @@ AGENT_AUTH_PROVIDER_PAGE_SIZE = 100
23
24
  AGENT_AUTH_MAX_PROVIDER_PAGES = 20
24
25
 
25
26
  _HTTP_BAD_REQUEST = 400
27
+ _HTTP_FORBIDDEN = 403
26
28
  _HTTP_NOT_FOUND = 404
27
29
  _HTTP_CONFLICT = 409
28
30
 
@@ -33,21 +35,46 @@ __all__ = [
33
35
  "AgentAuthUnknownProviderError",
34
36
  "CredentialAuthorizationRequiredError",
35
37
  "PendingAuthorization",
38
+ "ResolvePrincipalIdentity",
36
39
  "WorkspaceConnection",
40
+ "clear_principal_resolve_cache_for_tests",
37
41
  ]
38
42
 
39
43
 
40
44
  @dataclass(frozen=True)
41
45
  class AgentAuthContext:
42
46
  base_url: str
43
- #: Deployment id, passed to oauth-authorization-sessions as ``agent_id``.
44
- agent_id: str
45
47
  #: Authenticated caller, used as ``owner_id`` for user credentials.
46
48
  principal_id: str
49
+ #: Deployment id when present. Required for agent-owned lookups; optional for user OAuth.
50
+ agent_id: str | None = None
47
51
  api_key: str | None = None
48
52
  workspace_id: str | None = None
49
- #: Agent Builder service JWT for workspace-owned credential material.
50
- service_key: str | None = None
53
+
54
+
55
+ class ResolvePrincipalIdentity(TypedDict):
56
+ provider: str
57
+ issuer: str
58
+ subject: str
59
+
60
+
61
+ _principal_resolve_cache: dict[str, str] = {}
62
+
63
+
64
+ def clear_principal_resolve_cache_for_tests() -> None:
65
+ """Drop cached principal resolutions. Test-only."""
66
+ _principal_resolve_cache.clear()
67
+
68
+
69
+ def _principal_resolve_cache_key(
70
+ context: AgentAuthContext, identity: ResolvePrincipalIdentity
71
+ ) -> str:
72
+ origin = context.base_url.rstrip("/")
73
+ workspace = (context.workspace_id or "").strip()
74
+ return (
75
+ f"{origin}\0{workspace}\0"
76
+ f"{identity['provider']}\0{identity['issuer']}\0{identity['subject']}"
77
+ )
51
78
 
52
79
 
53
80
  class PendingAuthorization(TypedDict):
@@ -116,6 +143,49 @@ class AgentAuth:
116
143
  self._timeout = timeout
117
144
  self._request: AgentAuthRequest = request if request is not None else self._urlopen_json
118
145
 
146
+ def resolve_principal(self, identity: ResolvePrincipalIdentity) -> str:
147
+ """Resolve a verified external identity to an Agent Auth principal UUID."""
148
+ provider = identity["provider"].strip()
149
+ issuer = identity["issuer"].strip()
150
+ subject = identity["subject"].strip()
151
+ if not provider or not issuer or not subject:
152
+ raise CredentialStoreError(
153
+ "Agent Auth principal resolve requires provider, issuer, and subject"
154
+ )
155
+ key = _principal_resolve_cache_key(
156
+ self.context, {"provider": provider, "issuer": issuer, "subject": subject}
157
+ )
158
+ cached = _principal_resolve_cache.get(key)
159
+ if cached is not None:
160
+ return cached
161
+ principal_id = self._resolve_principal_uncached(
162
+ {"provider": provider, "issuer": issuer, "subject": subject}
163
+ )
164
+ _principal_resolve_cache[key] = principal_id
165
+ return principal_id
166
+
167
+ def _resolve_principal_uncached(self, identity: ResolvePrincipalIdentity) -> str:
168
+ status, body = self._request(
169
+ "POST",
170
+ "/v1/agent-auth/principals",
171
+ body={
172
+ "provider": identity["provider"],
173
+ "issuer": identity["issuer"],
174
+ "subject": identity["subject"],
175
+ },
176
+ )
177
+ if status >= _HTTP_BAD_REQUEST:
178
+ raise CredentialStoreError("Agent Auth resolve principal failed", status, body)
179
+ principal = body.get("principal")
180
+ principal_id = ""
181
+ if isinstance(principal, Mapping):
182
+ raw = principal.get("id")
183
+ if isinstance(raw, str):
184
+ principal_id = raw.strip()
185
+ if not principal_id:
186
+ raise CredentialStoreError("Agent Auth resolve principal returned no principal id")
187
+ return principal_id
188
+
119
189
  def find_workspace_connection(self, slug: str) -> WorkspaceConnection | None:
120
190
  """List workspace connections and return the row for ``slug``, if any."""
121
191
  if not (self.context.workspace_id or "").strip():
@@ -134,7 +204,7 @@ class AgentAuth:
134
204
  f"/v1/agent-auth/connections?{urllib.parse.urlencode(query)}",
135
205
  )
136
206
  if status >= _HTTP_BAD_REQUEST:
137
- raise CredentialStoreError("Agent Auth list connections failed", status)
207
+ raise CredentialStoreError("Agent Auth list connections failed", status, body)
138
208
  raw_items = body.get("items")
139
209
  items = raw_items if isinstance(raw_items, list) else []
140
210
  for item in items:
@@ -170,31 +240,23 @@ class AgentAuth:
170
240
  def find_secret_credential_id(self, connection: WorkspaceConnection, slug: str) -> str | None:
171
241
  """Resolve the credential id for a secret connection row.
172
242
 
173
- With an Agent Builder service key, prefer GET-for-owner / workspace list.
174
- Personal API keys may only list agent-owned credentials.
243
+ Agent-owned connection declarations scope the credential to this
244
+ deployment. Workspace-owned credentials are deliberately not visible
245
+ to deployments.
175
246
  """
176
247
  if connection.kind != "secret":
177
248
  return None
178
- workspace_id = (self.context.workspace_id or "").strip()
179
- if not workspace_id:
249
+ agent_id = (self.context.agent_id or "").strip()
250
+ if not agent_id:
180
251
  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,
252
+ "connections are not configured: LANGSMITH_HOST_PROJECT_ID is not set"
194
253
  )
254
+ from_agent = self._credential_id_for_owner(connection.connection_id, "agent", agent_id)
255
+ if from_agent is not None:
256
+ return from_agent
195
257
  return self.find_credential_id(
196
258
  owner_type="agent",
197
- owner_id=self.context.agent_id,
259
+ owner_id=agent_id,
198
260
  kind="secret",
199
261
  slug=slug,
200
262
  )
@@ -202,6 +264,12 @@ class AgentAuth:
202
264
  def _credential_id_for_owner(
203
265
  self, connection_id: str, owner_type: str, owner_id: str
204
266
  ) -> str | None:
267
+ """Read credential_id from GET /connections/{id}?credential_owner_*.
268
+
269
+ Deployments authenticate with ``LANGSMITH_API_KEY`` (the minted
270
+ service-account key). User and workspace owners additionally require
271
+ workspace management permission.
272
+ """
205
273
  query = urllib.parse.urlencode(
206
274
  {
207
275
  "credential_owner_type": owner_type,
@@ -215,16 +283,27 @@ class AgentAuth:
215
283
  if status == _HTTP_NOT_FOUND:
216
284
  return None
217
285
  if status >= _HTTP_BAD_REQUEST:
218
- raise CredentialStoreError("Agent Auth get connection for owner failed", status)
286
+ raise CredentialStoreError(
287
+ "Agent Auth get connection for owner failed"
288
+ + (" (forbidden)" if status == _HTTP_FORBIDDEN else ""),
289
+ status,
290
+ body,
291
+ )
219
292
  return _connection_credential_id(body)
220
293
 
221
294
  def read_user_secret(self, slug: str) -> str | None:
222
295
  """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,
296
+ connection = self.find_workspace_connection(slug)
297
+ if connection is None:
298
+ return None
299
+ return self.read_user_secret_for_connection(connection)
300
+
301
+ def read_user_secret_for_connection(self, connection: WorkspaceConnection) -> str | None:
302
+ """Read a caller-owned opaque secret when the connection row is already loaded."""
303
+ if connection.kind != "secret":
304
+ return None
305
+ credential_id = self._credential_id_for_owner(
306
+ connection.connection_id, "user", self.context.principal_id
228
307
  )
229
308
  if credential_id is None:
230
309
  return None
@@ -244,11 +323,17 @@ class AgentAuth:
244
323
  Returns:
245
324
  The access token, or ``None`` when the caller must connect.
246
325
  """
247
- credential_id = self.find_credential_id(
248
- owner_type="user",
249
- owner_id=self.context.principal_id,
250
- kind="oauth2",
251
- slug=slug,
326
+ connection = self.find_workspace_connection(slug)
327
+ if connection is None:
328
+ return None
329
+ return self.read_user_access_token_for_connection(connection)
330
+
331
+ def read_user_access_token_for_connection(self, connection: WorkspaceConnection) -> str | None:
332
+ """Read a caller OAuth token when the connection row is already loaded."""
333
+ if connection.kind != "oauth2":
334
+ return None
335
+ credential_id = self._credential_id_for_owner(
336
+ connection.connection_id, "user", self.context.principal_id
252
337
  )
253
338
  if credential_id is None:
254
339
  return None
@@ -258,7 +343,7 @@ class AgentAuth:
258
343
  """Start an OAuth session for a slug the caller has not granted."""
259
344
  connection = self.find_workspace_connection(slug)
260
345
  if connection is None:
261
- raise CredentialStoreError(f"workspace connection {slug!r} is not configured")
346
+ raise missing_workspace_connection_error(slug)
262
347
  if connection.kind != "oauth2":
263
348
  raise CredentialStoreError(f"connection {slug!r} is not an OAuth connection")
264
349
  status, body = self._request(
@@ -266,13 +351,13 @@ class AgentAuth:
266
351
  "/v1/agent-auth/oauth-authorization-sessions",
267
352
  body={
268
353
  "connection_id": connection.connection_id,
269
- "principal_id": self.context.principal_id,
270
- "agent_id": self.context.agent_id,
354
+ "owner_type": "user",
355
+ "owner_id": self.context.principal_id,
271
356
  },
272
357
  )
273
358
  if status >= _HTTP_BAD_REQUEST:
274
359
  raise CredentialStoreError(
275
- "Agent Auth create oauth-authorization-session failed", status
360
+ "Agent Auth create oauth-authorization-session failed", status, body
276
361
  )
277
362
  auth_id = str(body.get("id") or "")
278
363
  connect_url = str(body.get("verification_url") or "")
@@ -293,7 +378,7 @@ class AgentAuth:
293
378
  "GET", f"/v1/agent-auth/oauth-providers?{urllib.parse.urlencode(query)}"
294
379
  )
295
380
  if status >= _HTTP_BAD_REQUEST:
296
- raise CredentialStoreError("Agent Auth list oauth-providers failed", status)
381
+ raise CredentialStoreError("Agent Auth list oauth-providers failed", status, body)
297
382
  raw_items = body.get("items")
298
383
  for item in raw_items if isinstance(raw_items, list) else []:
299
384
  if isinstance(item, Mapping) and item.get("slug") == slug and item.get("id"):
@@ -329,7 +414,7 @@ class AgentAuth:
329
414
  )
330
415
  status, body = self._request("GET", f"/v1/agent-auth/credentials?{query}")
331
416
  if status >= _HTTP_BAD_REQUEST:
332
- raise CredentialStoreError("Agent Auth list credentials failed", status)
417
+ raise CredentialStoreError("Agent Auth list credentials failed", status, body)
333
418
  raw_items = body.get("items")
334
419
  items = raw_items if isinstance(raw_items, list) else []
335
420
  for item in items:
@@ -352,7 +437,7 @@ class AgentAuth:
352
437
  if status == _HTTP_NOT_FOUND:
353
438
  return None
354
439
  if status >= _HTTP_BAD_REQUEST:
355
- raise CredentialStoreError("Agent Auth get secret failed", status)
440
+ raise CredentialStoreError("Agent Auth get secret failed", status, body)
356
441
  secret = body.get("secret")
357
442
  return secret if isinstance(secret, str) and secret else None
358
443
 
@@ -364,7 +449,7 @@ class AgentAuth:
364
449
  if _is_authorization_required(status, body):
365
450
  return None
366
451
  if status >= _HTTP_BAD_REQUEST:
367
- raise CredentialStoreError("Agent Auth get oauth2 failed", status)
452
+ raise CredentialStoreError("Agent Auth get oauth2 failed", status, body)
368
453
  oauth2 = body.get("oauth2") if isinstance(body.get("oauth2"), dict) else body
369
454
  token = oauth2.get("access_token") if isinstance(oauth2, dict) else None
370
455
  return token if isinstance(token, str) and token else None
@@ -390,7 +475,6 @@ class AgentAuth:
390
475
  return agent_auth_headers(
391
476
  api_key=self.context.api_key,
392
477
  workspace_id=self.context.workspace_id,
393
- service_key=self.context.service_key,
394
478
  )
395
479
 
396
480
 
@@ -7,36 +7,96 @@ API with the same headers, redirect policy, and page walker.
7
7
  from __future__ import annotations
8
8
 
9
9
  import json
10
+ import re
10
11
  import urllib.error
11
12
  import urllib.parse
12
13
  import urllib.request
13
14
  from collections.abc import Mapping
14
- from typing import TYPE_CHECKING
15
+ from typing import TYPE_CHECKING, Final
15
16
 
16
17
  if TYPE_CHECKING:
17
18
  from http.client import HTTPMessage
18
19
  from typing import IO
19
20
 
20
21
  AGENT_AUTH_TIMEOUT_SECONDS = 10.0
22
+ AGENT_AUTH_PROBLEM_HINT_MAX_CHARS = 200
23
+ _PROBLEM_HINT_FIELDS: Final = ("detail", "remedy")
24
+
25
+
26
+ def agent_auth_problem_hint(body: Mapping[str, object] | None) -> str | None:
27
+ """Return the caller-facing part of an Agent Auth problem+json body.
28
+
29
+ ``detail`` names the failure and ``remedy`` names the fix. Nothing else
30
+ from a response body is read, because the rest can quote secret material.
31
+ Both are collapsed to one line and truncated so a long or multi-line body
32
+ cannot reshape an error message.
33
+ """
34
+ seen: dict[str, None] = {}
35
+ for field in _PROBLEM_HINT_FIELDS:
36
+ value = None if body is None else body.get(field)
37
+ if not isinstance(value, str):
38
+ continue
39
+ text = " ".join(value.split())
40
+ if text:
41
+ seen[text] = None
42
+ if not seen:
43
+ return None
44
+ hint = " ".join(seen)
45
+ if len(hint) > AGENT_AUTH_PROBLEM_HINT_MAX_CHARS:
46
+ return hint[:AGENT_AUTH_PROBLEM_HINT_MAX_CHARS] + "..."
47
+ return hint
21
48
 
22
49
 
23
50
  class CredentialStoreError(RuntimeError):
24
51
  """Agent Auth could not answer.
25
52
 
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.
53
+ Callers pass a fixed message; the only part of a response body that reaches
54
+ this error, the logs, or the traceback chain is the allow-listed
55
+ problem+json hint, since an arbitrary body can quote secret material.
28
56
  """
29
57
 
30
- def __init__(self, message: str, status: int | None = None) -> None:
58
+ def __init__(
59
+ self,
60
+ message: str,
61
+ status: int | None = None,
62
+ problem: Mapping[str, object] | None = None,
63
+ ) -> None:
31
64
  self.status = status
32
- super().__init__(message if status is None else f"{message} (status {status})")
65
+ with_status = message if status is None else f"{message} (status {status})"
66
+ hint = agent_auth_problem_hint(problem)
67
+ super().__init__(with_status if hint is None else f"{with_status}: {hint}")
68
+
69
+
70
+ # Each runtime quotes the slug its own way: repr() here, JSON.stringify in npm.
71
+ _MISSING_WORKSPACE_CONNECTION = re.compile(
72
+ r"""workspace connection ['"]([A-Za-z0-9][A-Za-z0-9._-]*)['"] is not configured"""
73
+ )
74
+
75
+
76
+ def missing_workspace_connection_error(slug: str) -> CredentialStoreError:
77
+ """Return the error for a declared slug that has no workspace connection row."""
78
+ return CredentialStoreError(f"workspace connection {slug!r} is not configured")
79
+
80
+
81
+ def missing_workspace_connection_slug(text: str) -> str | None:
82
+ """Read the slug back out of a missing-connection message.
83
+
84
+ A run that fails in the gate reaches a channel as a plain string, so this is
85
+ the only way back to the slug. It must move with the message it parses.
86
+ """
87
+ match = _MISSING_WORKSPACE_CONNECTION.search(text)
88
+ return None if match is None else match.group(1)
33
89
 
34
90
 
35
91
  __all__ = [
92
+ "AGENT_AUTH_PROBLEM_HINT_MAX_CHARS",
36
93
  "AGENT_AUTH_TIMEOUT_SECONDS",
37
94
  "CredentialStoreError",
38
95
  "agent_auth_credential_query",
39
96
  "agent_auth_headers",
97
+ "agent_auth_problem_hint",
98
+ "missing_workspace_connection_error",
99
+ "missing_workspace_connection_slug",
40
100
  "request_agent_auth_json",
41
101
  ]
42
102
 
@@ -70,16 +130,12 @@ def agent_auth_headers(
70
130
  *,
71
131
  api_key: str | None = None,
72
132
  workspace_id: str | None = None,
73
- service_key: str | None = None,
74
133
  ) -> dict[str, str]:
75
134
  headers = {
76
135
  "accept": "application/json",
77
136
  "content-type": "application/json",
78
137
  "x-langsmith-product": "mda",
79
138
  }
80
- resolved_service_key = (service_key or "").strip()
81
- if resolved_service_key:
82
- headers["x-service-key"] = resolved_service_key
83
139
  resolved_api_key = (api_key or "").strip()
84
140
  if resolved_api_key:
85
141
  headers["x-api-key"] = resolved_api_key
Binary file