managed-deepagents 0.6.2.dev50__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.
@@ -35,21 +35,48 @@ __all__ = [
35
35
  "AgentAuthUnknownProviderError",
36
36
  "CredentialAuthorizationRequiredError",
37
37
  "PendingAuthorization",
38
+ "ResolvePrincipalIdentity",
38
39
  "WorkspaceConnection",
40
+ "clear_principal_resolve_cache_for_tests",
39
41
  ]
40
42
 
41
43
 
42
44
  @dataclass(frozen=True)
43
45
  class AgentAuthContext:
44
46
  base_url: str
45
- #: Deployment id, passed to oauth-authorization-sessions as ``agent_id``.
46
- agent_id: str
47
47
  #: Authenticated caller, used as ``owner_id`` for user credentials.
48
48
  principal_id: str
49
+ #: Deployment id when present. Required for agent-owned lookups; optional for user OAuth.
50
+ agent_id: str | None = None
49
51
  api_key: str | None = None
50
52
  workspace_id: str | None = None
51
53
 
52
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
+ )
78
+
79
+
53
80
  class PendingAuthorization(TypedDict):
54
81
  """One connection slug the caller has not granted, with its connect URL."""
55
82
 
@@ -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():
@@ -176,14 +246,17 @@ class AgentAuth:
176
246
  """
177
247
  if connection.kind != "secret":
178
248
  return None
179
- from_agent = self._credential_id_for_owner(
180
- connection.connection_id, "agent", self.context.agent_id
181
- )
249
+ agent_id = (self.context.agent_id or "").strip()
250
+ if not agent_id:
251
+ raise CredentialStoreError(
252
+ "connections are not configured: LANGSMITH_HOST_PROJECT_ID is not set"
253
+ )
254
+ from_agent = self._credential_id_for_owner(connection.connection_id, "agent", agent_id)
182
255
  if from_agent is not None:
183
256
  return from_agent
184
257
  return self.find_credential_id(
185
258
  owner_type="agent",
186
- owner_id=self.context.agent_id,
259
+ owner_id=agent_id,
187
260
  kind="secret",
188
261
  slug=slug,
189
262
  )
@@ -278,8 +351,8 @@ class AgentAuth:
278
351
  "/v1/agent-auth/oauth-authorization-sessions",
279
352
  body={
280
353
  "connection_id": connection.connection_id,
281
- "principal_id": self.context.principal_id,
282
- "agent_id": self.context.agent_id,
354
+ "owner_type": "user",
355
+ "owner_id": self.context.principal_id,
283
356
  },
284
357
  )
285
358
  if status >= _HTTP_BAD_REQUEST:
Binary file
@@ -2,11 +2,11 @@
2
2
 
3
3
  ``{ type: "agent" }`` resolves material owned by the current deployment.
4
4
  ``{ type: "user" }`` resolves per-caller grants (OAuth or opaque). Resolution
5
- reads Agent Auth on every await and never caches.
5
+ reads Agent Auth on every await and never caches material.
6
6
 
7
7
  ``mda dev`` substitutes local values for agent opaque secrets via ``MDA_DEV_*``.
8
- User-owned connections need a deployment; opaque user grants use the same
9
- pre-run gate as OAuth.
8
+ User-owned connections talk to Agent Auth in every environment (including
9
+ ``mda dev``) after resolving the caller's Agent Auth principal.
10
10
  """
11
11
 
12
12
  from __future__ import annotations
@@ -24,7 +24,9 @@ from managed_deepagents._agent_auth import (
24
24
  AgentAuth,
25
25
  AgentAuthContext,
26
26
  CredentialAuthorizationRequiredError,
27
+ ResolvePrincipalIdentity,
27
28
  WorkspaceConnection,
29
+ clear_principal_resolve_cache_for_tests,
28
30
  )
29
31
  from managed_deepagents._agent_auth_http import (
30
32
  AGENT_AUTH_TIMEOUT_SECONDS,
@@ -49,10 +51,12 @@ __all__ = [
49
51
  "CredentialStoreError",
50
52
  "InvalidAgentAuthEndpointError",
51
53
  "agent_connection_env_key",
54
+ "clear_principal_resolve_cache_for_tests",
52
55
  "connections",
53
56
  "is_connection_reference",
54
57
  "resolve_agent_auth_base_url",
55
58
  "resolve_agent_auth_context",
59
+ "resolve_credential_principal_id",
56
60
  "resolve_owner_id",
57
61
  ]
58
62
 
@@ -108,19 +112,7 @@ class ConnectionNotFoundError(LookupError):
108
112
  _SLUG_PATTERN = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*")
109
113
  _MAX_SLUG_LENGTH = 64
110
114
  _DEV_CREDENTIAL_PREFIX = "MDA_DEV_"
111
-
112
-
113
- def _local_user_connection_unsupported(slug: str | None = None) -> str:
114
- local_hint = (
115
- f"set {agent_connection_env_key(slug)}"
116
- if slug
117
- else "set the corresponding MDA_DEV_<slug> variable"
118
- )
119
- return (
120
- "user-owned connections need a deployment, so they do not resolve in `mda dev`: "
121
- "deploy with `mda deploy` to exercise this connection, or declare it as "
122
- f'{{"type": "agent"}} and {local_hint} for local runs'
123
- )
115
+ _TRUSTED_BACKEND_PROVIDER = "trusted_backend"
124
116
 
125
117
 
126
118
  #: Env var naming the LangSmith platform API that serves Agent Auth.
@@ -131,6 +123,8 @@ _DEFAULT_LANGSMITH_ENDPOINT = "https://api.smith.langchain.com"
131
123
  DEPLOYMENT_ID_ENV = "LANGSMITH_HOST_PROJECT_ID"
132
124
  _API_KEY_ENV = "LANGSMITH_API_KEY"
133
125
  _WORKSPACE_ID_ENV = "LANGSMITH_WORKSPACE_ID"
126
+ _LOCAL_DEV_USER = "mda:local-dev"
127
+ _LOCAL_DEV_PRINCIPAL_ENV = "MDA_AGENT_AUTH_PRINCIPAL_ID"
134
128
 
135
129
 
136
130
  class InvalidAgentAuthEndpointError(CredentialStoreError):
@@ -157,6 +151,7 @@ class _AgentAuthConfig:
157
151
  api_key: str
158
152
  deployment_id: str | None
159
153
  workspace_id: str
154
+ local_dev_principal_id: str | None
160
155
 
161
156
 
162
157
  class AgentAuthConnectionStore:
@@ -176,15 +171,30 @@ class AgentAuthConnectionStore:
176
171
  async def fetch(self, request: ConnectionRequest) -> str | None:
177
172
  owner = request["owner"]
178
173
  resolved = os.environ if self._env is None else self._env
179
- if resolved.get("MDA_LOCAL_DEV") == "1":
180
- return _fetch_local_dev_connection(resolved, request["slug"], owner)
174
+ if resolved.get("MDA_LOCAL_DEV") == "1" and owner["type"] == "agent":
175
+ return _fetch_local_dev_agent_connection(resolved, request["slug"])
181
176
  config = _resolve_agent_auth_config(self._env)
182
- owner_id = resolve_owner_id(owner, request["identity"], config.deployment_id)
177
+ if owner["type"] == "agent" and not config.deployment_id:
178
+ raise CredentialStoreError(
179
+ f"connections are not configured: {DEPLOYMENT_ID_ENV} is not set"
180
+ )
181
+ # Source-identity match still uses JWT sub / channel actor.
182
+ resolve_owner_id(owner, request["identity"], config.deployment_id)
183
+ if owner["type"] == "user":
184
+ principal_id = await asyncio.to_thread(
185
+ resolve_credential_principal_id,
186
+ request["identity"],
187
+ config,
188
+ request_fn=self._bind_request(config),
189
+ timeout=self._timeout,
190
+ )
191
+ else:
192
+ principal_id = resolve_owner_id(owner, request["identity"], config.deployment_id)
183
193
  client = AgentAuth(
184
194
  AgentAuthContext(
185
195
  base_url=config.base_url,
186
- agent_id=config.deployment_id or owner_id,
187
- principal_id=owner_id,
196
+ agent_id=config.deployment_id,
197
+ principal_id=principal_id,
188
198
  api_key=config.api_key,
189
199
  workspace_id=config.workspace_id,
190
200
  ),
@@ -208,12 +218,14 @@ class AgentAuthConnectionStore:
208
218
  *,
209
219
  body: dict[str, object] | None = None,
210
220
  ) -> tuple[int, dict[str, object]]:
211
- del method, body
212
221
  url = f"{config.base_url}{path}"
213
222
  headers = agent_auth_headers(
214
223
  api_key=config.api_key,
215
224
  workspace_id=config.workspace_id,
216
225
  )
226
+ # Test injects only support GET-shaped responses; path still distinguishes
227
+ # principals vs connections for mock routers.
228
+ del method, body
217
229
  response = http_get(url, headers, timeout)
218
230
  status = response.get("status")
219
231
  raw_body = response.get("body", b"")
@@ -227,13 +239,7 @@ class AgentAuthConnectionStore:
227
239
  return request
228
240
 
229
241
 
230
- def _fetch_local_dev_connection(
231
- resolved: Mapping[str, str],
232
- slug: str,
233
- owner: ConnectionOwner,
234
- ) -> str:
235
- if owner["type"] == "user":
236
- raise CredentialStoreError(_local_user_connection_unsupported(slug))
242
+ def _fetch_local_dev_agent_connection(resolved: Mapping[str, str], slug: str) -> str:
237
243
  value = resolved.get(agent_connection_env_key(slug))
238
244
  if value is not None and value.strip():
239
245
  return value
@@ -410,24 +416,104 @@ def resolve_owner_id(
410
416
  def resolve_agent_auth_context(
411
417
  identity: RuntimeIdentity | None,
412
418
  env: Mapping[str, str] | None = None,
419
+ *,
420
+ request_fn: Callable[..., tuple[int, dict[str, object]]] | None = None,
421
+ timeout: float = AGENT_AUTH_TIMEOUT_SECONDS,
413
422
  ) -> AgentAuthContext:
414
- resolved = os.environ if env is None else env
415
- if resolved.get("MDA_LOCAL_DEV") == "1":
416
- raise CredentialStoreError(_local_user_connection_unsupported())
417
423
  config = _resolve_agent_auth_config(env)
418
- if not config.deployment_id:
419
- raise CredentialStoreError(
420
- f"connections are not configured: {DEPLOYMENT_ID_ENV} is not set"
421
- )
424
+ principal_id = resolve_credential_principal_id(
425
+ identity, config, request_fn=request_fn, timeout=timeout
426
+ )
422
427
  return AgentAuthContext(
423
428
  base_url=config.base_url,
424
429
  agent_id=config.deployment_id,
425
- principal_id=resolve_owner_id({"type": "user"}, identity, config.deployment_id),
430
+ principal_id=principal_id,
426
431
  api_key=config.api_key,
427
432
  workspace_id=config.workspace_id,
428
433
  )
429
434
 
430
435
 
436
+ def resolve_credential_principal_id(
437
+ identity: RuntimeIdentity | None,
438
+ config: _AgentAuthConfig,
439
+ *,
440
+ request_fn: Callable[..., tuple[int, dict[str, object]]] | None = None,
441
+ timeout: float = AGENT_AUTH_TIMEOUT_SECONDS,
442
+ ) -> str:
443
+ """Map a runtime identity to the Agent Auth principal used for user grants."""
444
+ if identity is None:
445
+ raise CredentialStoreError("user connections require an authenticated user principal")
446
+ user = identity.get("user") or {}
447
+ user_id = user.get("id") if isinstance(user, Mapping) else None
448
+ if not isinstance(user_id, str) or not user_id.strip():
449
+ raise CredentialStoreError("user connections require an authenticated user principal")
450
+ kind = user.get("kind") if isinstance(user, Mapping) else None
451
+ if kind == "service":
452
+ if user_id.strip() == _LOCAL_DEV_USER and config.local_dev_principal_id is not None:
453
+ return config.local_dev_principal_id
454
+ raise CredentialStoreError(
455
+ "user-owned connections require a person principal; "
456
+ "Studio and other service callers cannot own OAuth grants"
457
+ )
458
+ stamped = identity.get("agent_auth_principal_id")
459
+ if isinstance(stamped, str) and stamped.strip():
460
+ return stamped.strip()
461
+
462
+ subject = user_id.strip()
463
+ external = _external_identity_for_resolve(identity, config.workspace_id)
464
+ if external is None:
465
+ raise CredentialStoreError(
466
+ "user-owned connections need a resolvable identity "
467
+ "(validated-token issuer/subject, channel identity_context, or trusted backend)"
468
+ )
469
+ bootstrap = AgentAuth(
470
+ AgentAuthContext(
471
+ base_url=config.base_url,
472
+ principal_id=subject,
473
+ api_key=config.api_key,
474
+ workspace_id=config.workspace_id,
475
+ agent_id=config.deployment_id,
476
+ ),
477
+ request=request_fn,
478
+ timeout=timeout,
479
+ )
480
+ return bootstrap.resolve_principal(external)
481
+
482
+
483
+ def _external_identity_for_resolve(
484
+ identity: RuntimeIdentity,
485
+ workspace_id: str | None,
486
+ ) -> ResolvePrincipalIdentity | None:
487
+ provider = identity.get("identity_provider")
488
+ issuer = identity.get("identity_issuer")
489
+ subject = identity.get("identity_subject")
490
+ if (
491
+ isinstance(provider, str)
492
+ and provider.strip()
493
+ and isinstance(issuer, str)
494
+ and issuer.strip()
495
+ and isinstance(subject, str)
496
+ and subject.strip()
497
+ ):
498
+ return {
499
+ "provider": provider.strip(),
500
+ "issuer": issuer.strip(),
501
+ "subject": subject.strip(),
502
+ }
503
+ user = identity.get("user") or {}
504
+ trusted_subject = user.get("id") if isinstance(user, Mapping) else None
505
+ if not isinstance(trusted_subject, str) or not trusted_subject.strip():
506
+ return None
507
+ workspace = (workspace_id or "").strip()
508
+ if not workspace:
509
+ return None
510
+ return {
511
+ "provider": _TRUSTED_BACKEND_PROVIDER,
512
+ "issuer": workspace,
513
+ "subject": trusted_subject.strip(),
514
+ }
515
+
516
+
431
517
  def _resolve_agent_auth_config(env: Mapping[str, str] | None) -> _AgentAuthConfig:
432
518
  resolved = os.environ if env is None else env
433
519
  api_key = (resolved.get(_API_KEY_ENV) or "").strip()
@@ -443,6 +529,11 @@ def _resolve_agent_auth_config(env: Mapping[str, str] | None) -> _AgentAuthConfi
443
529
  api_key=api_key,
444
530
  deployment_id=(resolved.get(DEPLOYMENT_ID_ENV) or "").strip() or None,
445
531
  workspace_id=workspace_id,
532
+ local_dev_principal_id=(
533
+ (resolved.get(_LOCAL_DEV_PRINCIPAL_ENV) or "").strip() or None
534
+ if resolved.get("MDA_LOCAL_DEV") == "1"
535
+ else None
536
+ ),
446
537
  )
447
538
 
448
539
 
@@ -106,7 +106,7 @@ def create_credential_gate_middleware(
106
106
  _gate(consumers, caller.context, payload)
107
107
 
108
108
  async def abefore_agent(self, _state: object, runtime: object | None = None) -> None:
109
- caller = _require_auth_context(runtime)
109
+ caller = await asyncio.to_thread(_require_auth_context, runtime)
110
110
  payload = await asyncio.to_thread(_pending_interrupt, consumers, caller)
111
111
  _gate(consumers, caller.context, payload)
112
112
 
@@ -58,6 +58,14 @@ class ManagedUser(ManagedUserRequired, total=False):
58
58
  mda_source_provider: str
59
59
  mda_source_thread_id: str
60
60
  mda_user_kind: Literal["user", "service"]
61
+ #: Validated-token provider id for Agent Auth principal resolve.
62
+ mda_identity_provider: str
63
+ #: Verified IdP issuer (``iss``) for Agent Auth principal resolve.
64
+ mda_identity_issuer: str
65
+ #: Verified IdP subject (``sub``) for Agent Auth principal resolve.
66
+ mda_identity_subject: str
67
+ #: Agent Auth principal UUID when Trigger ``identity_context`` already resolved one.
68
+ mda_agent_auth_principal_id: str
61
69
 
62
70
 
63
71
  if TYPE_CHECKING:
@@ -88,6 +96,8 @@ INGRESS_SECRET_HEADER = "x-mda-ingress-secret" # noqa: S105 - header name, not
88
96
  #: Internal handoff headers — accepted after a server API key or legacy ingress secret validates.
89
97
  SOURCE_PROVIDER_HEADER = "x-mda-source-provider"
90
98
  SOURCE_THREAD_HEADER = "x-mda-source-thread-id"
99
+ #: Trigger loopback stamp for a verified identity_context principal UUID.
100
+ AGENT_AUTH_PRINCIPAL_HEADER = "x-mda-agent-auth-principal-id"
91
101
 
92
102
  #: Deployment env var carrying the shared secret the trusted backend presents.
93
103
  INGRESS_SECRET_ENV = "MDA_INGRESS_SECRET" # noqa: S105 - env var name, not a secret
@@ -477,6 +487,9 @@ def _trusted_headers_user(headers: Mapping[str, object]) -> ManagedUser:
477
487
  if source_provider is not None and source_thread is not None:
478
488
  user["mda_source_provider"] = str(source_provider)
479
489
  user["mda_source_thread_id"] = str(source_thread)
490
+ principal = str(headers.get(AGENT_AUTH_PRINCIPAL_HEADER) or "").strip()
491
+ if principal:
492
+ user["mda_agent_auth_principal_id"] = principal
480
493
  return user
481
494
 
482
495
 
@@ -497,7 +510,13 @@ def _has_valid_handoff_secret(headers: Mapping[str, object]) -> bool:
497
510
 
498
511
  def _has_handoff_headers(headers: Mapping[str, object]) -> bool:
499
512
  return any(
500
- header in headers for header in (USER_HEADER, SOURCE_PROVIDER_HEADER, SOURCE_THREAD_HEADER)
513
+ header in headers
514
+ for header in (
515
+ USER_HEADER,
516
+ SOURCE_PROVIDER_HEADER,
517
+ SOURCE_THREAD_HEADER,
518
+ AGENT_AUTH_PRINCIPAL_HEADER,
519
+ )
501
520
  )
502
521
 
503
522
 
@@ -455,10 +455,27 @@ def build_runtime_identity(configurable: Mapping[str, object] | None) -> Runtime
455
455
  claims = _field(user, "mda_claims")
456
456
  if isinstance(claims, Mapping):
457
457
  identity["claims"] = dict(cast("Mapping[str, object]", claims))
458
+ _stamp_agent_auth_identity(identity, user)
458
459
  # Immutable: tools/middleware get a read-only view they cannot tamper with.
459
460
  return cast("RuntimeIdentity", _freeze(identity))
460
461
 
461
462
 
463
+ def _stamp_agent_auth_identity(identity: RuntimeIdentity, user: object) -> None:
464
+ """Copy validated-token / channel Agent Auth stamps onto the envelope."""
465
+ provider = _field(user, "mda_identity_provider")
466
+ if isinstance(provider, str) and provider.strip():
467
+ identity["identity_provider"] = provider.strip()
468
+ issuer = _field(user, "mda_identity_issuer")
469
+ if isinstance(issuer, str) and issuer.strip():
470
+ identity["identity_issuer"] = issuer.strip()
471
+ subject = _field(user, "mda_identity_subject")
472
+ if isinstance(subject, str) and subject.strip():
473
+ identity["identity_subject"] = subject.strip()
474
+ principal_id = _field(user, "mda_agent_auth_principal_id")
475
+ if isinstance(principal_id, str) and principal_id.strip():
476
+ identity["agent_auth_principal_id"] = principal_id.strip()
477
+
478
+
462
479
  def _is_studio_auth_user(user: object) -> bool:
463
480
  """True when LangGraph Platform / Studio supplied the auth user."""
464
481
  if not isinstance(user, Mapping):
@@ -19,6 +19,7 @@ from managed_deepagents._channels.assistant_text import extract_assistant_text
19
19
  from managed_deepagents._channels.binding import CHANNEL_BINDING_KEY, ChannelBinding
20
20
  from managed_deepagents._channels.types import ChannelError
21
21
  from managed_deepagents._identity_auth import (
22
+ AGENT_AUTH_PRINCIPAL_HEADER,
22
23
  HANDOFF_SECRET_ENV,
23
24
  HANDOFF_SECRET_HEADER,
24
25
  INGRESS_SECRET_ENV,
@@ -355,6 +356,7 @@ def _create_trigger_ingress_loopback_client(
355
356
  )
356
357
  headers = {
357
358
  USER_HEADER: user_id,
359
+ AGENT_AUTH_PRINCIPAL_HEADER: user_id,
358
360
  SOURCE_PROVIDER_HEADER: source["provider"],
359
361
  SOURCE_THREAD_HEADER: source["thread_key"],
360
362
  }
@@ -258,11 +258,17 @@ def claims_to_user(provider: ValidatedTokenProvider, claims: Mapping[str, object
258
258
  caller = claims.get(user_claim)
259
259
  if caller in (None, ""):
260
260
  raise ManagedAuthError(401, f'token missing user claim "{user_claim}"')
261
+ subject = _verified_identity_subject(claims)
262
+ issuer = _verified_identity_issuer(provider, claims)
261
263
  user: ManagedUser = {
262
264
  "identity": str(caller),
263
265
  "permissions": [],
264
266
  "mda_user_id": str(caller),
265
267
  "mda_claims": dict(claims),
268
+ # Provider id + verified iss/sub drive Agent Auth POST /principals.
269
+ "mda_identity_provider": (provider.get("id") or "").strip() or "oidc",
270
+ "mda_identity_issuer": issuer,
271
+ "mda_identity_subject": subject,
266
272
  }
267
273
  groups = normalize_groups(claims.get(claim_map["groups"])) if claim_map.get("groups") else []
268
274
  if groups:
@@ -273,6 +279,35 @@ def claims_to_user(provider: ValidatedTokenProvider, claims: Mapping[str, object
273
279
  return user
274
280
 
275
281
 
282
+ def _verified_identity_subject(claims: Mapping[str, object]) -> str:
283
+ """OIDC ``sub``, or Supabase introspection ``id`` (same stable value)."""
284
+ raw_sub = claims.get("sub")
285
+ if isinstance(raw_sub, str) and raw_sub.strip():
286
+ return raw_sub.strip()
287
+ raw_id = claims.get("id")
288
+ if isinstance(raw_id, str) and raw_id.strip():
289
+ return raw_id.strip()
290
+ raise ManagedAuthError(401, 'token missing "sub" claim')
291
+
292
+
293
+ def _verified_identity_issuer(
294
+ provider: ValidatedTokenProvider, claims: Mapping[str, object]
295
+ ) -> str:
296
+ """Token ``iss``, else the provider's configured issuer / first ``issuers`` entry."""
297
+ raw_iss = claims.get("iss")
298
+ if isinstance(raw_iss, str) and raw_iss.strip():
299
+ return raw_iss.strip()
300
+ configured = provider.get("issuer")
301
+ if isinstance(configured, str) and configured.strip():
302
+ return configured.strip()
303
+ issuers = provider.get("issuers")
304
+ if isinstance(issuers, Sequence) and not isinstance(issuers, (str, bytes)):
305
+ for entry in issuers:
306
+ if isinstance(entry, str) and entry.strip():
307
+ return entry.strip()
308
+ raise ManagedAuthError(401, 'token missing "iss" claim')
309
+
310
+
276
311
  def _verify_provider_claims(
277
312
  provider: ValidatedTokenProvider, token: str, headers: Mapping[str, object]
278
313
  ) -> Mapping[str, object]:
@@ -177,6 +177,16 @@ class RuntimeIdentity(RuntimeIdentityRequired, total=False):
177
177
  #: tools and middleware.
178
178
  groups: tuple[str, ...]
179
179
  claims: JsonObject
180
+ #: Validated-token provider id for Agent Auth principal resolve.
181
+ identity_provider: str
182
+ #: Verified IdP issuer (``iss``) for Agent Auth principal resolve.
183
+ identity_issuer: str
184
+ #: Verified IdP subject (``sub``) for Agent Auth principal resolve.
185
+ #: Stamped so grants match other LangSmith clients even when ``claims.user``
186
+ #: maps to email. Thread ownership still uses ``user.id``.
187
+ identity_subject: str
188
+ #: Agent Auth principal UUID when channel ingress already resolved one.
189
+ agent_auth_principal_id: str
180
190
 
181
191
 
182
192
  class PrincipalRequired(TypedDict):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: managed-deepagents
3
- Version: 0.6.2.dev50
3
+ Version: 0.6.2.dev52
4
4
  Summary: Managed Deep Agents — the define_deep_agent authoring interface plus the CLI that compiles and deploys a code-first Deep Agent repository to a managed LangGraph runtime.
5
5
  Author: LangChain
6
6
  License: MIT
@@ -277,10 +277,14 @@ connector = connectors.mcp(
277
277
  For this example, set `MDA_DEV_DOCS_TOKEN`.
278
278
 
279
279
  A user-owned connection — `connections.get(slug, {"type": "user"})` — resolves
280
- per caller (OAuth or opaque). The pre-run credential gate interrupts the run
281
- when a grant is missing. `mda dev` has no deployment to hold user grants, so
282
- a user-owned connection fails there with an actionable error; deploy the agent
283
- to exercise one.
280
+ per caller (OAuth or opaque). The runtime maps the verified ingress identity to
281
+ an Agent Auth principal, then the pre-run credential gate interrupts the run
282
+ when a grant is missing. That path works in `mda deploy` and `mda dev` when
283
+ `LANGSMITH_API_KEY` and `LANGSMITH_WORKSPACE_ID` are set and the workspace
284
+ connection rows exist — use a person principal (for example a Supabase JWT),
285
+ not Studio’s synthetic service user. OAuth completes on LangSmith’s platform
286
+ callback URL; the local UI must surface `credential_authorization_required`
287
+ and resume after the user connects.
284
288
 
285
289
  Deploy the project first, then provision its connections with
286
290
  `mda connections create`. Agent-owned opaque secrets are scoped to that
@@ -1,26 +1,26 @@
1
1
  managed_deepagents/__init__.py,sha256=RezkpgYxCcywj6_W7Nk3PN9TMBfYyFgIatxGrIeY8V4,1862
2
2
  managed_deepagents/__main__.py,sha256=GcjmqN0Ea5v8OHKMxYToasm3eCX_doSLOlpEzAi1tAw,67
3
- managed_deepagents/_agent_auth.py,sha256=DCGZNfMUpUEwLQT7tIODywmtzO3XjF4a1dixuDR_Fzs,17266
3
+ managed_deepagents/_agent_auth.py,sha256=8QiCiGxYjbk3BTrOeCGVkQTLh6IzTUia18-_Mry9NWI,20018
4
4
  managed_deepagents/_agent_auth_http.py,sha256=d-Fg62m-_5qBXj-d9xgfA3KGiPRfs7T2g6Q2nrhCetc,6065
5
5
  managed_deepagents/_channel.py,sha256=-Og7QOM1yvV-yeTDOfv2CxXpR2HLY4xdX1bLrIsPi5k,5272
6
6
  managed_deepagents/_cli.py,sha256=jVaw2i1ysdRoWziMdPFmRXtKry_baYGRspyzyIeGWoU,1688
7
- managed_deepagents/_connections.py,sha256=VwVBwM4T0rp1MgYu9D8QjGj1ireYeEuP3VIAFN5urFY,16246
7
+ managed_deepagents/_connections.py,sha256=B0mrBbh0dhFxFMkdkYVAM5Q5WaKUcYJryizZh_Hxg3A,19963
8
8
  managed_deepagents/_connector.py,sha256=Syp0SRs1oFCUUltIUmZbSNi1Ys_PxObVdiAi-I9GBzs,8040
9
9
  managed_deepagents/_cors.py,sha256=V96RPrqewdRWG5RpEMEBnPuRq9_zF8nf4vf99k5dkuI,7597
10
- managed_deepagents/_credential_gate.py,sha256=thc9dqWphPKbNqWYEZ1P63mpMDyUPDTw4uPA_qqnW28,12262
10
+ managed_deepagents/_credential_gate.py,sha256=VLjSc7k1MoZdMI8juZpdeOK7lYyvbb9OsFZGAhoj6t4,12287
11
11
  managed_deepagents/_eval_filesystem.py,sha256=1nuD0yn3gh19JrWNocWyPFyeJW42E7Gse3XRWCUSTAA,1399
12
12
  managed_deepagents/_extension.py,sha256=x2H1S8inkvTZQ_dr7kq0M8o8rre1FjzCDvMzItw4iyk,5318
13
- managed_deepagents/_identity_auth.py,sha256=l0t0Xx-J1-z_Zw4K5D-B2yV6lNdeJgOw7UvxODxUxZQ,32798
13
+ managed_deepagents/_identity_auth.py,sha256=mQ-4pVFZRc9uweFqi7LEzAUtzhuz5fcUHVak0LSzz64,33620
14
14
  managed_deepagents/_identity_http.py,sha256=aVQfZr7UKQoXF1Dry3p9fzXIj1882sEa668rvhn37i0,16029
15
15
  managed_deepagents/_identity_links.py,sha256=Z1H2Seo2KaI-vcdU3_PGM-F06wOz0IQqUP-28wqiQsA,9172
16
- managed_deepagents/_identity_runtime.py,sha256=vsshj0TNWd-rXUhVsPAqXIkANunQT7jI94CWgzq-1cY,24409
17
- managed_deepagents/_ingress.py,sha256=iqsGm2jZKKn0FqjNn2nJAlR7d-IqCpVKWo6NbMEhjxw,12323
16
+ managed_deepagents/_identity_runtime.py,sha256=URaYgx7ef5ihxySWrQBDI_KnP0sB1WXNfK55PnB3imI,25287
17
+ managed_deepagents/_ingress.py,sha256=8vJqIuyKI3dxC5wbyRPL9ES71xsXgxiTWJqYPpG7Wt0,12402
18
18
  managed_deepagents/_log.py,sha256=oYwOO0ZL4-INkge9YJvtLuCxL9zAtgjSPIggVbkcyc8,2932
19
19
  managed_deepagents/_loopback.py,sha256=hQ1oNi77XpdUjzZURSBRoMvJ1mGCs-STobfBftGmXuE,2409
20
20
  managed_deepagents/_managed_middleware.py,sha256=cfInVHTVjWoy_DBoFlnslfrDf2_n6llGnTGasta4AIo,10295
21
21
  managed_deepagents/_managed_tools.py,sha256=KsFDULbv5fv0RY3Nj0zbv6z0Y80_o5YazhqeiQ9PnPk,9105
22
22
  managed_deepagents/_memory_instructions.py,sha256=lyxKfNuXrWih574gmy3NHhIq_hIpJIM5zl3P8V69mUs,6678
23
- managed_deepagents/_validated_token.py,sha256=BY-66bgAOxhhRS3tfkDWVVU2l9HFHAuYXXCKV1pZ-nE,17232
23
+ managed_deepagents/_validated_token.py,sha256=On96IOQTDz1KE_ZZmuTqY8IPw-cJ3J2tXkkmrfyvbB4,18762
24
24
  managed_deepagents/define_deep_agent.py,sha256=17VfiiBINyhlqlATHrlJRPihKVlBsDGDP8K4-fxjxU8,5611
25
25
  managed_deepagents/define_memory.py,sha256=n7Ya20dWGvwbZOkPIXoa68nTnJie8tpKEkRwfJQt_fU,5036
26
26
  managed_deepagents/namespaces.py,sha256=GS76G19-V5Svfpbca-9xhQfJ6vel52jcJyfj6loLt_Y,4199
@@ -28,7 +28,7 @@ managed_deepagents/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
28
28
  managed_deepagents/runtime.py,sha256=B4iKTJZD0zXx_MZ09pqQFOkVo07tqJDT3qvOBr-0iOk,65782
29
29
  managed_deepagents/sandbox.py,sha256=xlpcWa6CoVX6GKip2nK-0Lw-HBCoFGFYm0r4x9eYwcY,2530
30
30
  managed_deepagents/schedules.py,sha256=MtTQN4tlm1NizFDCjZZv_huGfbQx-dj9WP1dIBybCug,5237
31
- managed_deepagents/_binaries/mda.exe,sha256=MKHDoU4yrPlrylogVcQ8MmsJKXs7r6MzRI3OPV6z5fg,3904512
31
+ managed_deepagents/_binaries/mda.exe,sha256=b51_ocM9YjizSP3IFh0105T7lXdFD8BxK3ZDYxe3nJk,3911680
32
32
  managed_deepagents/_channels/__init__.py,sha256=Z6eXmzaLxG09DSyJoY_CX34axcsBo9gSJ0G7pRm2D6M,1216
33
33
  managed_deepagents/_channels/assistant_text.py,sha256=f-Z3J3PlQFXZHztgBxlRoqB2LigU1Hj1YpwoXBMTR18,2171
34
34
  managed_deepagents/_channels/binding.py,sha256=6vAKbWpg-eyLErJ0SsozH4mZc50hA1NhquM63mVmWDQ,3423
@@ -65,13 +65,13 @@ managed_deepagents/_memory/mounts.py,sha256=b2KtaTX3apTkmoqQxG2ENtkO86avSt1Nvrv3
65
65
  managed_deepagents/_memory/paths.py,sha256=gHI-9KyawUv1ftX1K0kjaBbM4IYLeP7S-_WCaS2gVBM,353
66
66
  managed_deepagents/_memory/slices.py,sha256=ljL78ECmDZ27xc3ajzkLliOfUln_bNr-d1ycC1wlS38,3559
67
67
  managed_deepagents/_memory/types.py,sha256=scSk6mH0k2OC59wzUCa67szvXylSjWDrfzWWRIU9tZA,4336
68
- managed_deepagents/identity/__init__.py,sha256=Slm-oEqKxbaZEQsMqHfD3LbG5M4vH2lKUmralVPB_Zs,16137
68
+ managed_deepagents/identity/__init__.py,sha256=RKuFKCN3Ui2ggUzdntYhnEdZRf-vT1khU4e-8Me2XIY,16680
69
69
  managed_deepagents/identity/claims.py,sha256=prby7r_8Lww3MmoBm8PX6PsSGE3GwSHkr8mVIz_08nU,1180
70
70
  managed_deepagents/identity/options.py,sha256=x_yMbXEFR7zXyehEjDHG2qjAlukXjrRBcQLcM6IndLM,2550
71
71
  managed_deepagents/identity/supabase.py,sha256=8uobl3WCrQL_ieSXG8KTLf1th7Qjim9liZT50HeaJE0,6026
72
- managed_deepagents-0.6.2.dev50.dist-info/licenses/LICENSE,sha256=WnPZQQ608ojZUkXl7BW6VbKloIAibZiGm1K6cjeZOL8,1066
73
- managed_deepagents-0.6.2.dev50.dist-info/METADATA,sha256=jGDCHaAI4JMsFMrZ_fhKDqt6PVYObwGB3vD_HoY_fxU,13718
74
- managed_deepagents-0.6.2.dev50.dist-info/WHEEL,sha256=3I5VVWZdsFlU417aCS2bMRuerS8wsfvBq99MyatBV3A,97
75
- managed_deepagents-0.6.2.dev50.dist-info/entry_points.txt,sha256=ziZqkKuaDO3qHCjKmBhrx9cm3ok8bZ9xcEDpUTj4unM,53
76
- managed_deepagents-0.6.2.dev50.dist-info/top_level.txt,sha256=fdBo4O0Hlol_w022tTLB5ftZpamb4yJoQ3MUvjgTGfA,19
77
- managed_deepagents-0.6.2.dev50.dist-info/RECORD,,
72
+ managed_deepagents-0.6.2.dev52.dist-info/licenses/LICENSE,sha256=WnPZQQ608ojZUkXl7BW6VbKloIAibZiGm1K6cjeZOL8,1066
73
+ managed_deepagents-0.6.2.dev52.dist-info/METADATA,sha256=hJuiAgthvUvvFJlQ7Yj43cnbE1mgwjzIBlKVJMJ1T_c,14048
74
+ managed_deepagents-0.6.2.dev52.dist-info/WHEEL,sha256=3I5VVWZdsFlU417aCS2bMRuerS8wsfvBq99MyatBV3A,97
75
+ managed_deepagents-0.6.2.dev52.dist-info/entry_points.txt,sha256=ziZqkKuaDO3qHCjKmBhrx9cm3ok8bZ9xcEDpUTj4unM,53
76
+ managed_deepagents-0.6.2.dev52.dist-info/top_level.txt,sha256=fdBo4O0Hlol_w022tTLB5ftZpamb4yJoQ3MUvjgTGfA,19
77
+ managed_deepagents-0.6.2.dev52.dist-info/RECORD,,