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.
- managed_deepagents/_agent_auth.py +81 -8
- managed_deepagents/_binaries/mda.exe +0 -0
- managed_deepagents/_connections.py +128 -37
- managed_deepagents/_credential_gate.py +1 -1
- managed_deepagents/_identity_auth.py +20 -1
- managed_deepagents/_identity_runtime.py +17 -0
- managed_deepagents/_ingress.py +2 -0
- managed_deepagents/_validated_token.py +35 -0
- managed_deepagents/identity/__init__.py +10 -0
- {managed_deepagents-0.6.2.dev50.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/METADATA +9 -5
- {managed_deepagents-0.6.2.dev50.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/RECORD +15 -15
- {managed_deepagents-0.6.2.dev50.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/WHEEL +0 -0
- {managed_deepagents-0.6.2.dev50.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/entry_points.txt +0 -0
- {managed_deepagents-0.6.2.dev50.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/licenses/LICENSE +0 -0
- {managed_deepagents-0.6.2.dev50.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
180
|
-
|
|
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=
|
|
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
|
-
"
|
|
282
|
-
"
|
|
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
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
187
|
-
principal_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
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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=
|
|
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
|
|
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
|
|
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):
|
managed_deepagents/_ingress.py
CHANGED
|
@@ -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):
|
{managed_deepagents-0.6.2.dev50.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: managed-deepagents
|
|
3
|
-
Version: 0.6.2.
|
|
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
|
|
281
|
-
|
|
282
|
-
a
|
|
283
|
-
|
|
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
|
{managed_deepagents-0.6.2.dev50.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/RECORD
RENAMED
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
17
|
-
managed_deepagents/_ingress.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
73
|
-
managed_deepagents-0.6.2.
|
|
74
|
-
managed_deepagents-0.6.2.
|
|
75
|
-
managed_deepagents-0.6.2.
|
|
76
|
-
managed_deepagents-0.6.2.
|
|
77
|
-
managed_deepagents-0.6.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{managed_deepagents-0.6.2.dev50.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/top_level.txt
RENAMED
|
File without changes
|