managed-deepagents 0.6.2.dev34__py3-none-win_amd64.whl → 0.6.2.dev36__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.
@@ -23,6 +23,7 @@ AGENT_AUTH_PROVIDER_PAGE_SIZE = 100
23
23
  AGENT_AUTH_MAX_PROVIDER_PAGES = 20
24
24
 
25
25
  _HTTP_BAD_REQUEST = 400
26
+ _HTTP_FORBIDDEN = 403
26
27
  _HTTP_NOT_FOUND = 404
27
28
  _HTTP_CONFLICT = 409
28
29
 
@@ -46,8 +47,6 @@ class AgentAuthContext:
46
47
  principal_id: str
47
48
  api_key: str | None = None
48
49
  workspace_id: str | None = None
49
- #: Agent Builder service JWT for workspace-owned credential material.
50
- service_key: str | None = None
51
50
 
52
51
 
53
52
  class PendingAuthorization(TypedDict):
@@ -134,7 +133,7 @@ class AgentAuth:
134
133
  f"/v1/agent-auth/connections?{urllib.parse.urlencode(query)}",
135
134
  )
136
135
  if status >= _HTTP_BAD_REQUEST:
137
- raise CredentialStoreError("Agent Auth list connections failed", status)
136
+ raise CredentialStoreError("Agent Auth list connections failed", status, body)
138
137
  raw_items = body.get("items")
139
138
  items = raw_items if isinstance(raw_items, list) else []
140
139
  for item in items:
@@ -170,28 +169,17 @@ class AgentAuth:
170
169
  def find_secret_credential_id(self, connection: WorkspaceConnection, slug: str) -> str | None:
171
170
  """Resolve the credential id for a secret connection row.
172
171
 
173
- With an Agent Builder service key, prefer GET-for-owner / workspace list.
174
- Personal API keys may only list agent-owned credentials.
172
+ Agent-owned connection declarations scope the credential to this
173
+ deployment. Workspace-owned credentials are deliberately not visible
174
+ to deployments.
175
175
  """
176
176
  if connection.kind != "secret":
177
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
- )
178
+ from_agent = self._credential_id_for_owner(
179
+ connection.connection_id, "agent", self.context.agent_id
180
+ )
181
+ if from_agent is not None:
182
+ return from_agent
195
183
  return self.find_credential_id(
196
184
  owner_type="agent",
197
185
  owner_id=self.context.agent_id,
@@ -202,6 +190,12 @@ class AgentAuth:
202
190
  def _credential_id_for_owner(
203
191
  self, connection_id: str, owner_type: str, owner_id: str
204
192
  ) -> str | None:
193
+ """Read credential_id from GET /connections/{id}?credential_owner_*.
194
+
195
+ Deployments authenticate with ``LANGSMITH_API_KEY`` (the minted
196
+ service-account key). User and workspace owners additionally require
197
+ workspace management permission.
198
+ """
205
199
  query = urllib.parse.urlencode(
206
200
  {
207
201
  "credential_owner_type": owner_type,
@@ -215,16 +209,27 @@ class AgentAuth:
215
209
  if status == _HTTP_NOT_FOUND:
216
210
  return None
217
211
  if status >= _HTTP_BAD_REQUEST:
218
- raise CredentialStoreError("Agent Auth get connection for owner failed", status)
212
+ raise CredentialStoreError(
213
+ "Agent Auth get connection for owner failed"
214
+ + (" (forbidden)" if status == _HTTP_FORBIDDEN else ""),
215
+ status,
216
+ body,
217
+ )
219
218
  return _connection_credential_id(body)
220
219
 
221
220
  def read_user_secret(self, slug: str) -> str | None:
222
221
  """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,
222
+ connection = self.find_workspace_connection(slug)
223
+ if connection is None:
224
+ return None
225
+ return self.read_user_secret_for_connection(connection)
226
+
227
+ def read_user_secret_for_connection(self, connection: WorkspaceConnection) -> str | None:
228
+ """Read a caller-owned opaque secret when the connection row is already loaded."""
229
+ if connection.kind != "secret":
230
+ return None
231
+ credential_id = self._credential_id_for_owner(
232
+ connection.connection_id, "user", self.context.principal_id
228
233
  )
229
234
  if credential_id is None:
230
235
  return None
@@ -244,11 +249,17 @@ class AgentAuth:
244
249
  Returns:
245
250
  The access token, or ``None`` when the caller must connect.
246
251
  """
247
- credential_id = self.find_credential_id(
248
- owner_type="user",
249
- owner_id=self.context.principal_id,
250
- kind="oauth2",
251
- slug=slug,
252
+ connection = self.find_workspace_connection(slug)
253
+ if connection is None:
254
+ return None
255
+ return self.read_user_access_token_for_connection(connection)
256
+
257
+ def read_user_access_token_for_connection(self, connection: WorkspaceConnection) -> str | None:
258
+ """Read a caller OAuth token when the connection row is already loaded."""
259
+ if connection.kind != "oauth2":
260
+ return None
261
+ credential_id = self._credential_id_for_owner(
262
+ connection.connection_id, "user", self.context.principal_id
252
263
  )
253
264
  if credential_id is None:
254
265
  return None
@@ -272,7 +283,7 @@ class AgentAuth:
272
283
  )
273
284
  if status >= _HTTP_BAD_REQUEST:
274
285
  raise CredentialStoreError(
275
- "Agent Auth create oauth-authorization-session failed", status
286
+ "Agent Auth create oauth-authorization-session failed", status, body
276
287
  )
277
288
  auth_id = str(body.get("id") or "")
278
289
  connect_url = str(body.get("verification_url") or "")
@@ -293,7 +304,7 @@ class AgentAuth:
293
304
  "GET", f"/v1/agent-auth/oauth-providers?{urllib.parse.urlencode(query)}"
294
305
  )
295
306
  if status >= _HTTP_BAD_REQUEST:
296
- raise CredentialStoreError("Agent Auth list oauth-providers failed", status)
307
+ raise CredentialStoreError("Agent Auth list oauth-providers failed", status, body)
297
308
  raw_items = body.get("items")
298
309
  for item in raw_items if isinstance(raw_items, list) else []:
299
310
  if isinstance(item, Mapping) and item.get("slug") == slug and item.get("id"):
@@ -329,7 +340,7 @@ class AgentAuth:
329
340
  )
330
341
  status, body = self._request("GET", f"/v1/agent-auth/credentials?{query}")
331
342
  if status >= _HTTP_BAD_REQUEST:
332
- raise CredentialStoreError("Agent Auth list credentials failed", status)
343
+ raise CredentialStoreError("Agent Auth list credentials failed", status, body)
333
344
  raw_items = body.get("items")
334
345
  items = raw_items if isinstance(raw_items, list) else []
335
346
  for item in items:
@@ -352,7 +363,7 @@ class AgentAuth:
352
363
  if status == _HTTP_NOT_FOUND:
353
364
  return None
354
365
  if status >= _HTTP_BAD_REQUEST:
355
- raise CredentialStoreError("Agent Auth get secret failed", status)
366
+ raise CredentialStoreError("Agent Auth get secret failed", status, body)
356
367
  secret = body.get("secret")
357
368
  return secret if isinstance(secret, str) and secret else None
358
369
 
@@ -364,7 +375,7 @@ class AgentAuth:
364
375
  if _is_authorization_required(status, body):
365
376
  return None
366
377
  if status >= _HTTP_BAD_REQUEST:
367
- raise CredentialStoreError("Agent Auth get oauth2 failed", status)
378
+ raise CredentialStoreError("Agent Auth get oauth2 failed", status, body)
368
379
  oauth2 = body.get("oauth2") if isinstance(body.get("oauth2"), dict) else body
369
380
  token = oauth2.get("access_token") if isinstance(oauth2, dict) else None
370
381
  return token if isinstance(token, str) and token else None
@@ -390,7 +401,6 @@ class AgentAuth:
390
401
  return agent_auth_headers(
391
402
  api_key=self.context.api_key,
392
403
  workspace_id=self.context.workspace_id,
393
- service_key=self.context.service_key,
394
404
  )
395
405
 
396
406
 
@@ -11,32 +11,68 @@ import urllib.error
11
11
  import urllib.parse
12
12
  import urllib.request
13
13
  from collections.abc import Mapping
14
- from typing import TYPE_CHECKING
14
+ from typing import TYPE_CHECKING, Final
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  from http.client import HTTPMessage
18
18
  from typing import IO
19
19
 
20
20
  AGENT_AUTH_TIMEOUT_SECONDS = 10.0
21
+ AGENT_AUTH_PROBLEM_HINT_MAX_CHARS = 200
22
+ _PROBLEM_HINT_FIELDS: Final = ("detail", "remedy")
23
+
24
+
25
+ def agent_auth_problem_hint(body: Mapping[str, object] | None) -> str | None:
26
+ """Return the caller-facing part of an Agent Auth problem+json body.
27
+
28
+ ``detail`` names the failure and ``remedy`` names the fix. Nothing else
29
+ from a response body is read, because the rest can quote secret material.
30
+ Both are collapsed to one line and truncated so a long or multi-line body
31
+ cannot reshape an error message.
32
+ """
33
+ seen: dict[str, None] = {}
34
+ for field in _PROBLEM_HINT_FIELDS:
35
+ value = None if body is None else body.get(field)
36
+ if not isinstance(value, str):
37
+ continue
38
+ text = " ".join(value.split())
39
+ if text:
40
+ seen[text] = None
41
+ if not seen:
42
+ return None
43
+ hint = " ".join(seen)
44
+ if len(hint) > AGENT_AUTH_PROBLEM_HINT_MAX_CHARS:
45
+ return hint[:AGENT_AUTH_PROBLEM_HINT_MAX_CHARS] + "..."
46
+ return hint
21
47
 
22
48
 
23
49
  class CredentialStoreError(RuntimeError):
24
50
  """Agent Auth could not answer.
25
51
 
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.
52
+ Callers pass a fixed message; the only part of a response body that reaches
53
+ this error, the logs, or the traceback chain is the allow-listed
54
+ problem+json hint, since an arbitrary body can quote secret material.
28
55
  """
29
56
 
30
- def __init__(self, message: str, status: int | None = None) -> None:
57
+ def __init__(
58
+ self,
59
+ message: str,
60
+ status: int | None = None,
61
+ problem: Mapping[str, object] | None = None,
62
+ ) -> None:
31
63
  self.status = status
32
- super().__init__(message if status is None else f"{message} (status {status})")
64
+ with_status = message if status is None else f"{message} (status {status})"
65
+ hint = agent_auth_problem_hint(problem)
66
+ super().__init__(with_status if hint is None else f"{with_status}: {hint}")
33
67
 
34
68
 
35
69
  __all__ = [
70
+ "AGENT_AUTH_PROBLEM_HINT_MAX_CHARS",
36
71
  "AGENT_AUTH_TIMEOUT_SECONDS",
37
72
  "CredentialStoreError",
38
73
  "agent_auth_credential_query",
39
74
  "agent_auth_headers",
75
+ "agent_auth_problem_hint",
40
76
  "request_agent_auth_json",
41
77
  ]
42
78
 
@@ -70,16 +106,12 @@ def agent_auth_headers(
70
106
  *,
71
107
  api_key: str | None = None,
72
108
  workspace_id: str | None = None,
73
- service_key: str | None = None,
74
109
  ) -> dict[str, str]:
75
110
  headers = {
76
111
  "accept": "application/json",
77
112
  "content-type": "application/json",
78
113
  "x-langsmith-product": "mda",
79
114
  }
80
- resolved_service_key = (service_key or "").strip()
81
- if resolved_service_key:
82
- headers["x-service-key"] = resolved_service_key
83
115
  resolved_api_key = (api_key or "").strip()
84
116
  if resolved_api_key:
85
117
  headers["x-api-key"] = resolved_api_key
Binary file
@@ -16,6 +16,12 @@ from managed_deepagents._channels.trigger.client import (
16
16
  TriggerClientLike,
17
17
  create_trigger_client,
18
18
  )
19
+ from managed_deepagents._channels.trigger.identity_context import (
20
+ IdentityContextKey,
21
+ IdentityContextUnavailableError,
22
+ InvalidIdentityContextError,
23
+ principal_id_from_identity_context,
24
+ )
19
25
  from managed_deepagents._channels.trigger.types import (
20
26
  TRIGGER_SERVICE_KEY_HEADER,
21
27
  TriggerChannelEvent,
@@ -52,6 +58,7 @@ class StartAgentFn(Protocol):
52
58
  messages: list[dict[str, object]],
53
59
  source_provider: str,
54
60
  source_thread_key: str,
61
+ user_id: str,
55
62
  ) -> StartedIngressRun: ...
56
63
 
57
64
 
@@ -64,6 +71,7 @@ class ResumeAgentFn(Protocol):
64
71
  resume: TriggerChannelResume,
65
72
  source_provider: str,
66
73
  source_thread_key: str,
74
+ user_id: str,
67
75
  ) -> StartedIngressRun: ...
68
76
 
69
77
 
@@ -74,6 +82,7 @@ async def _start_event_run(
74
82
  source_provider: str,
75
83
  start_agent: StartAgentFn | None,
76
84
  resume_agent: ResumeAgentFn | None,
85
+ user_id: str,
77
86
  ) -> StartedIngressRun:
78
87
  thread_id = _thread_id(event["source_thread_key"])
79
88
  if event["type"] == "message":
@@ -84,6 +93,7 @@ async def _start_event_run(
84
93
  messages=event["input"]["messages"],
85
94
  source_provider=source_provider,
86
95
  source_thread_key=event["source_thread_key"],
96
+ user_id=user_id,
87
97
  )
88
98
  resume = resume_agent or resume_ingress_agent
89
99
  return await resume(
@@ -92,10 +102,11 @@ async def _start_event_run(
92
102
  resume=event["interaction"]["resume"],
93
103
  source_provider=source_provider,
94
104
  source_thread_key=event["source_thread_key"],
105
+ user_id=user_id,
95
106
  )
96
107
 
97
108
 
98
- async def handle_trigger_channel_event_request(
109
+ async def handle_trigger_channel_event_request( # noqa: C901, PLR0911
99
110
  request: object,
100
111
  *,
101
112
  source_provider: str,
@@ -106,9 +117,11 @@ async def handle_trigger_channel_event_request(
106
117
  resume_agent: ResumeAgentFn | None = None,
107
118
  observe_agent: ObserveAgentFn | None = None,
108
119
  trigger_client: TriggerClientLike | None = None,
120
+ identity_context_key: IdentityContextKey | None = None,
121
+ identity_context_issuer: str | None = None,
109
122
  ) -> object:
110
123
  """Validate a Trigger message and enqueue its Agent Server run."""
111
- # TODO: Validate a per-deployment Trigger↔MDA shared secret.
124
+ # TODO: Validate Trigger's platform-signed x-service-key JWT (not presence).
112
125
  if not _nonblank_header(request, TRIGGER_SERVICE_KEY_HEADER):
113
126
  return _json_response({"error": "unauthorized"}, status_code=401)
114
127
 
@@ -117,6 +130,32 @@ async def handle_trigger_channel_event_request(
117
130
  except (TypeError, ValueError, UnicodeDecodeError):
118
131
  logger.warning("invalid channel event")
119
132
  return _json_response({"error": "invalid channel event"}, status_code=400)
133
+
134
+ try:
135
+ user_id = _principal_for_event(
136
+ event,
137
+ identity_context_key=identity_context_key,
138
+ identity_context_issuer=identity_context_issuer,
139
+ )
140
+ except _MissingIdentityContextError:
141
+ logger.warning("missing channel identity_context")
142
+ return _json_response({"error": "missing channel identity_context"}, status_code=400)
143
+ except IdentityContextUnavailableError as error:
144
+ logger.error(
145
+ "channel identity_context unavailable (%s)",
146
+ logger.safe_error_label(error),
147
+ )
148
+ return _json_response(
149
+ {"error": "identity_context verification unavailable"},
150
+ status_code=503,
151
+ )
152
+ except InvalidIdentityContextError as error:
153
+ logger.warning(
154
+ "invalid channel identity_context (%s)",
155
+ logger.safe_error_label(error),
156
+ )
157
+ return _json_response({"error": "invalid channel identity_context"}, status_code=400)
158
+
120
159
  logger.info(
121
160
  "Channel incoming event (type=%s, event_id=%s, delivery_id=%s, channel_id=%s)",
122
161
  event["type"],
@@ -159,6 +198,7 @@ async def handle_trigger_channel_event_request(
159
198
  source_provider=source_provider,
160
199
  start_agent=start_agent,
161
200
  resume_agent=resume_agent,
201
+ user_id=user_id,
162
202
  )
163
203
  except Exception as error: # noqa: BLE001 - never expose Agent Server details in the response
164
204
  logger.error(
@@ -173,6 +213,9 @@ async def handle_trigger_channel_event_request(
173
213
  started=started,
174
214
  client=client,
175
215
  observe_agent=observe_agent,
216
+ source_provider=source_provider,
217
+ source_thread_key=event["source_thread_key"],
218
+ user_id=user_id,
176
219
  )
177
220
 
178
221
  schedule_ingress_work(
@@ -192,6 +235,9 @@ async def _observe_accepted_run(
192
235
  started: StartedIngressRun,
193
236
  client: TriggerClientLike,
194
237
  observe_agent: ObserveAgentFn | None,
238
+ source_provider: str | None = None,
239
+ source_thread_key: str | None = None,
240
+ user_id: str,
195
241
  ) -> None:
196
242
  async def lifecycle(
197
243
  event_type: TriggerChannelRuntimeEventType,
@@ -212,11 +258,13 @@ async def _observe_accepted_run(
212
258
  )
213
259
 
214
260
  await lifecycle("run.started")
215
- observe = observe_agent or observe_ingress_agent
216
261
  try:
217
- observed = await observe(
218
- thread_id=started["thread_id"],
219
- run_id=started["run_id"],
262
+ observed = await _observe_run(
263
+ started=started,
264
+ observe_agent=observe_agent,
265
+ source_provider=source_provider,
266
+ source_thread_key=source_thread_key,
267
+ user_id=user_id,
220
268
  )
221
269
  except Exception: # noqa: BLE001 - observation details stay internal
222
270
  await lifecycle("run.failed")
@@ -263,6 +311,53 @@ async def _observe_accepted_run(
263
311
  await lifecycle("run.completed")
264
312
 
265
313
 
314
+ async def _observe_run(
315
+ *,
316
+ started: StartedIngressRun,
317
+ observe_agent: ObserveAgentFn | None,
318
+ source_provider: str | None,
319
+ source_thread_key: str | None,
320
+ user_id: str,
321
+ ) -> ObservedIngressRun:
322
+ if observe_agent is not None:
323
+ return await observe_agent(
324
+ **_observation_kwargs(
325
+ started=started,
326
+ source_provider=source_provider,
327
+ source_thread_key=source_thread_key,
328
+ user_id=user_id,
329
+ )
330
+ )
331
+ if source_provider is None or source_thread_key is None:
332
+ raise RuntimeError("channel observation context is not configured")
333
+ return await observe_ingress_agent(
334
+ thread_id=started["thread_id"],
335
+ run_id=started["run_id"],
336
+ source_provider=source_provider,
337
+ source_thread_key=source_thread_key,
338
+ user_id=user_id,
339
+ )
340
+
341
+
342
+ def _observation_kwargs(
343
+ *,
344
+ started: StartedIngressRun,
345
+ source_provider: str | None,
346
+ source_thread_key: str | None,
347
+ user_id: str,
348
+ ) -> dict[str, object]:
349
+ kwargs: dict[str, object] = {
350
+ "thread_id": started["thread_id"],
351
+ "run_id": started["run_id"],
352
+ "user_id": user_id,
353
+ }
354
+ if source_provider is None or source_thread_key is None:
355
+ return kwargs
356
+ kwargs["source_provider"] = source_provider
357
+ kwargs["source_thread_key"] = source_thread_key
358
+ return kwargs
359
+
360
+
266
361
  def _is_interrupted_run(observed: ObservedIngressRun) -> bool:
267
362
  # Agent Server can report status "success" after join while values still
268
363
  # carry `__interrupt__`. Trust either signal.
@@ -315,6 +410,27 @@ def _thread_id(source_thread_key: str) -> str:
315
410
  return str(uuid.uuid5(uuid.NAMESPACE_URL, source_thread_key))
316
411
 
317
412
 
413
+ class _MissingIdentityContextError(ValueError):
414
+ """A message event arrived without a Trigger identity_context token."""
415
+
416
+
417
+ def _principal_for_event(
418
+ event: TriggerChannelEvent,
419
+ *,
420
+ identity_context_key: IdentityContextKey | None,
421
+ identity_context_issuer: str | None,
422
+ ) -> str:
423
+ """Resolve the verified principal required on every Trigger delivery."""
424
+ raw_identity_context = event.get("identity_context")
425
+ if not isinstance(raw_identity_context, str) or not raw_identity_context.strip():
426
+ raise _MissingIdentityContextError
427
+ return principal_id_from_identity_context(
428
+ raw_identity_context,
429
+ key=identity_context_key,
430
+ issuer=identity_context_issuer,
431
+ )
432
+
433
+
318
434
  def _resolve_assistant_id(override: str | None) -> str:
319
435
  if override is not None and (explicit := override.strip()):
320
436
  return explicit
@@ -0,0 +1,214 @@
1
+ """Verify Trigger Server ``identity_context`` JWTs into Agent Auth principal ids.
2
+
3
+ Agent Auth signs these with the LangSmith EdDSA JWKS published at
4
+ ``{LANGSMITH_ENDPOINT}/.well-known/jwks.json``. MDA verifies the signature
5
+ (and typ/version/aud/iss/exp/principal UUID) before stamping ``x-mda-user-id``.
6
+ Delivery still requires Trigger's ``x-service-key`` header; signature verify is
7
+ what stops a forged principal when that gate is weak.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from collections.abc import Mapping
14
+ from uuid import UUID
15
+
16
+ import jwt
17
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
18
+ from jwt import PyJWK, PyJWKClient
19
+
20
+ IdentityContextKey = Ed25519PublicKey | PyJWK
21
+
22
+ IDENTITY_CONTEXT_TOKEN_TYP = "identity-context+jwt" # noqa: S105
23
+ IDENTITY_CONTEXT_AUDIENCE = "mda"
24
+ IDENTITY_CONTEXT_VERSION = "1"
25
+ IDENTITY_CONTEXT_ALGORITHMS = ("EdDSA",)
26
+ _DEFAULT_LANGSMITH_ENDPOINT = "https://api.smith.langchain.com"
27
+ _JWKS_HTTP_TIMEOUT_S = 5
28
+
29
+ _jwks_clients: dict[str, PyJWKClient] = {}
30
+
31
+
32
+ class InvalidIdentityContextError(ValueError):
33
+ """Raised when a Trigger ``identity_context`` token is unusable."""
34
+
35
+
36
+ class IdentityContextUnavailableError(RuntimeError):
37
+ """JWKS / network failure while verifying identity_context. Callers should 5xx."""
38
+
39
+
40
+ def resolve_identity_context_issuer(
41
+ env: Mapping[str, str] | None = None,
42
+ ) -> str:
43
+ """LangSmith API origin used as identity-context ``iss`` and JWKS host."""
44
+ source = env if env is not None else os.environ
45
+ raw = (
46
+ (source.get("LANGSMITH_ENDPOINT") or "").strip()
47
+ or (source.get("LANGCHAIN_ENDPOINT") or "").strip()
48
+ or _DEFAULT_LANGSMITH_ENDPOINT
49
+ )
50
+ return raw.rstrip("/")
51
+
52
+
53
+ def resolve_identity_context_jwks_url(
54
+ env: Mapping[str, str] | None = None,
55
+ ) -> str:
56
+ """Public JWKS URL for Agent Auth identity-context signatures."""
57
+ return f"{resolve_identity_context_issuer(env)}/.well-known/jwks.json"
58
+
59
+
60
+ def principal_id_from_identity_context(
61
+ token: str,
62
+ *,
63
+ key: IdentityContextKey | None = None,
64
+ issuer: str | None = None,
65
+ env: Mapping[str, str] | None = None,
66
+ ) -> str:
67
+ """Extract the Agent Auth principal UUID from a Trigger identity-context JWT.
68
+
69
+ Raises:
70
+ InvalidIdentityContextError: when the token is missing, malformed, or
71
+ fails signature / claim checks.
72
+ """
73
+ value = token.strip()
74
+ if not value:
75
+ raise InvalidIdentityContextError("identity_context is blank")
76
+
77
+ _require_identity_context_header(value)
78
+ claims = _verify_identity_context_claims(
79
+ value,
80
+ key=key,
81
+ issuer=issuer or resolve_identity_context_issuer(env),
82
+ env=env,
83
+ )
84
+ if claims.get("version") != IDENTITY_CONTEXT_VERSION:
85
+ raise InvalidIdentityContextError(
86
+ f"identity_context version must be {IDENTITY_CONTEXT_VERSION}"
87
+ )
88
+ return _principal_id_from_claims(claims)
89
+
90
+
91
+ def optional_principal_id_from_identity_context(
92
+ token: object | None,
93
+ *,
94
+ key: IdentityContextKey | None = None,
95
+ issuer: str | None = None,
96
+ env: Mapping[str, str] | None = None,
97
+ ) -> str | None:
98
+ """Resolve a principal id from an optional event field; ``None`` when absent."""
99
+ if token is None:
100
+ return None
101
+ if not isinstance(token, str):
102
+ raise InvalidIdentityContextError("identity_context must be a string")
103
+ if not token.strip():
104
+ return None
105
+ return principal_id_from_identity_context(token, key=key, issuer=issuer, env=env)
106
+
107
+
108
+ def _require_identity_context_header(token: str) -> None:
109
+ try:
110
+ header = jwt.get_unverified_header(token)
111
+ except jwt.exceptions.DecodeError as error:
112
+ raise InvalidIdentityContextError("identity_context is not a JWT") from error
113
+ if header.get("typ") != IDENTITY_CONTEXT_TOKEN_TYP:
114
+ raise InvalidIdentityContextError(
115
+ f"identity_context typ must be {IDENTITY_CONTEXT_TOKEN_TYP}"
116
+ )
117
+ if header.get("alg") != "EdDSA":
118
+ raise InvalidIdentityContextError("identity_context alg must be EdDSA")
119
+
120
+
121
+ def _verify_identity_context_claims(
122
+ token: str,
123
+ *,
124
+ key: IdentityContextKey | None,
125
+ issuer: str,
126
+ env: Mapping[str, str] | None,
127
+ ) -> dict[str, object]:
128
+ algorithms = list(IDENTITY_CONTEXT_ALGORITHMS)
129
+ try:
130
+ verify_key = _jwks_client(env).get_signing_key_from_jwt(token) if key is None else key
131
+ decoded = jwt.decode(
132
+ token,
133
+ key=verify_key,
134
+ algorithms=algorithms,
135
+ audience=IDENTITY_CONTEXT_AUDIENCE,
136
+ issuer=issuer,
137
+ options={"require": ["exp"], "verify_exp": True},
138
+ )
139
+ except jwt.exceptions.ExpiredSignatureError as error:
140
+ raise InvalidIdentityContextError("identity_context is expired") from error
141
+ except jwt.exceptions.MissingRequiredClaimError as error:
142
+ raise InvalidIdentityContextError("identity_context is missing exp") from error
143
+ except jwt.exceptions.PyJWKClientConnectionError as error:
144
+ raise IdentityContextUnavailableError(
145
+ "identity_context JWKS verification is unavailable"
146
+ ) from error
147
+ except TimeoutError as error:
148
+ raise IdentityContextUnavailableError(
149
+ "identity_context JWKS verification is unavailable"
150
+ ) from error
151
+ except OSError as error:
152
+ raise IdentityContextUnavailableError(
153
+ "identity_context JWKS verification is unavailable"
154
+ ) from error
155
+ except jwt.exceptions.PyJWTError as error:
156
+ raise InvalidIdentityContextError(
157
+ "identity_context signature verification failed"
158
+ ) from error
159
+ if not isinstance(decoded, dict):
160
+ raise InvalidIdentityContextError("identity_context claims must be an object")
161
+ claims = {str(name): value for name, value in decoded.items()}
162
+ if not isinstance(claims.get("exp"), (int, float)) or isinstance(claims.get("exp"), bool):
163
+ raise InvalidIdentityContextError("identity_context is missing exp")
164
+ return claims
165
+
166
+
167
+ def _jwks_client(env: Mapping[str, str] | None) -> PyJWKClient:
168
+ uri = resolve_identity_context_jwks_url(env)
169
+ client = _jwks_clients.get(uri)
170
+ if client is None:
171
+ client = PyJWKClient(uri, timeout=_JWKS_HTTP_TIMEOUT_S)
172
+ _jwks_clients[uri] = client
173
+ return client
174
+
175
+
176
+ def _principal_id_from_claims(claims: Mapping[str, object]) -> str:
177
+ from_principal = _principal_id_claim(claims.get("principal"))
178
+ raw_subject = claims.get("sub")
179
+ from_subject = _normalize_uuid(raw_subject) if isinstance(raw_subject, str) else None
180
+ if from_principal and from_subject and from_principal != from_subject:
181
+ raise InvalidIdentityContextError("identity_context principal.id and sub disagree")
182
+ principal_id = from_principal or from_subject
183
+ if not principal_id:
184
+ raise InvalidIdentityContextError("identity_context is missing a principal UUID")
185
+ return principal_id
186
+
187
+
188
+ def _principal_id_claim(value: object) -> str | None:
189
+ if not isinstance(value, Mapping):
190
+ return None
191
+ raw_id = value.get("id")
192
+ return _normalize_uuid(raw_id) if isinstance(raw_id, str) else None
193
+
194
+
195
+ def _normalize_uuid(value: str) -> str | None:
196
+ try:
197
+ return str(UUID(value))
198
+ except ValueError:
199
+ return None
200
+
201
+
202
+ __all__ = [
203
+ "IDENTITY_CONTEXT_ALGORITHMS",
204
+ "IDENTITY_CONTEXT_AUDIENCE",
205
+ "IDENTITY_CONTEXT_TOKEN_TYP",
206
+ "IDENTITY_CONTEXT_VERSION",
207
+ "IdentityContextKey",
208
+ "IdentityContextUnavailableError",
209
+ "InvalidIdentityContextError",
210
+ "optional_principal_id_from_identity_context",
211
+ "principal_id_from_identity_context",
212
+ "resolve_identity_context_issuer",
213
+ "resolve_identity_context_jwks_url",
214
+ ]
@@ -25,7 +25,7 @@ class TriggerChannelMessageInput(TypedDict):
25
25
  messages: list[dict[str, object]]
26
26
 
27
27
 
28
- class TriggerChannelMessageEvent(TypedDict):
28
+ class TriggerChannelMessageEventRequired(TypedDict):
29
29
  version: Literal["1"]
30
30
  type: Literal["message"]
31
31
  event_id: str
@@ -37,6 +37,10 @@ class TriggerChannelMessageEvent(TypedDict):
37
37
  input: TriggerChannelMessageInput
38
38
 
39
39
 
40
+ class TriggerChannelMessageEvent(TriggerChannelMessageEventRequired, total=False):
41
+ identity_context: str | None
42
+
43
+
40
44
  # Keyed LangGraph resume: `{interrupt_id: value}`. Values are opaque
41
45
  # (`interrupt()` results or HITL `{decisions}`).
42
46
  TriggerChannelResume: TypeAlias = dict[str, object]
@@ -47,7 +51,7 @@ class TriggerChannelInterruptResumeInteraction(TypedDict):
47
51
  resume: TriggerChannelResume
48
52
 
49
53
 
50
- class TriggerChannelInterruptResumeEvent(TypedDict):
54
+ class TriggerChannelInterruptResumeEventRequired(TypedDict):
51
55
  version: Literal["1"]
52
56
  type: Literal["interrupt_resume"]
53
57
  event_id: str
@@ -59,6 +63,10 @@ class TriggerChannelInterruptResumeEvent(TypedDict):
59
63
  interaction: TriggerChannelInterruptResumeInteraction
60
64
 
61
65
 
66
+ class TriggerChannelInterruptResumeEvent(TriggerChannelInterruptResumeEventRequired, total=False):
67
+ identity_context: str | None
68
+
69
+
62
70
  TriggerChannelEvent = TriggerChannelMessageEvent | TriggerChannelInterruptResumeEvent
63
71
 
64
72
 
@@ -170,6 +178,7 @@ class _MessageEventModel(_WireModel):
170
178
  source_thread_key: _NonBlankString
171
179
  address: SkipValidation[dict[str, object]]
172
180
  input: _InputModel
181
+ identity_context: str | None = None
173
182
 
174
183
  @field_validator("address", mode="before")
175
184
  @classmethod
@@ -197,6 +206,7 @@ class _InterruptResumeEventModel(_WireModel):
197
206
  source_thread_key: _NonBlankString
198
207
  address: SkipValidation[dict[str, object]]
199
208
  interaction: _InterruptResumeInteractionModel
209
+ identity_context: str | None = None
200
210
 
201
211
  @field_validator("address", mode="before")
202
212
  @classmethod
@@ -1,6 +1,6 @@
1
1
  """``connections.get(...)`` — read a managed connection from a tool.
2
2
 
3
- ``{ type: "agent" }`` resolves shared workspace material on the connection row.
3
+ ``{ type: "agent" }`` resolves material owned by the current deployment.
4
4
  ``{ type: "user" }`` resolves per-caller grants (OAuth or opaque). Resolution
5
5
  reads Agent Auth on every await and never caches.
6
6
 
@@ -157,11 +157,10 @@ class _AgentAuthConfig:
157
157
  api_key: str
158
158
  deployment_id: str | None
159
159
  workspace_id: str
160
- service_key: str | None = None
161
160
 
162
161
 
163
162
  class AgentAuthConnectionStore:
164
- """Store backed by workspace connection rows in Agent Auth."""
163
+ """Store backed by connection rows and deployment/user grants in Agent Auth."""
165
164
 
166
165
  def __init__(
167
166
  self,
@@ -188,7 +187,6 @@ class AgentAuthConnectionStore:
188
187
  principal_id=owner_id,
189
188
  api_key=config.api_key,
190
189
  workspace_id=config.workspace_id,
191
- service_key=config.service_key,
192
190
  ),
193
191
  request=self._bind_request(config),
194
192
  timeout=self._timeout,
@@ -215,7 +213,6 @@ class AgentAuthConnectionStore:
215
213
  headers = agent_auth_headers(
216
214
  api_key=config.api_key,
217
215
  workspace_id=config.workspace_id,
218
- service_key=config.service_key,
219
216
  )
220
217
  response = http_get(url, headers, timeout)
221
218
  status = response.get("status")
@@ -254,19 +251,18 @@ def _lookup_workspace_connection(
254
251
  if row is None:
255
252
  return None
256
253
  if owner["type"] == "user":
257
- return _lookup_user_connection_material(client, slug, row)
254
+ return _lookup_user_connection_material(client, row)
258
255
  return _lookup_agent_connection_material(client, slug, row)
259
256
 
260
257
 
261
258
  def _lookup_user_connection_material(
262
259
  client: AgentAuth,
263
- slug: str,
264
260
  row: WorkspaceConnection,
265
261
  ) -> str | None:
266
262
  if row.kind == "secret":
267
- return client.read_user_secret(slug)
263
+ return client.read_user_secret_for_connection(row)
268
264
  if row.kind == "oauth2":
269
- return client.read_user_access_token(slug)
265
+ return client.read_user_access_token_for_connection(row)
270
266
  return None
271
267
 
272
268
 
@@ -429,7 +425,6 @@ def resolve_agent_auth_context(
429
425
  principal_id=resolve_owner_id({"type": "user"}, identity, config.deployment_id),
430
426
  api_key=config.api_key,
431
427
  workspace_id=config.workspace_id,
432
- service_key=config.service_key,
433
428
  )
434
429
 
435
430
 
@@ -443,13 +438,11 @@ def _resolve_agent_auth_config(env: Mapping[str, str] | None) -> _AgentAuthConfi
443
438
  raise CredentialStoreError(
444
439
  f"connections are not configured: {_WORKSPACE_ID_ENV} is not set"
445
440
  )
446
- service_key = (resolved.get("MDA_AGENT_AUTH_SERVICE_KEY") or "").strip() or None
447
441
  return _AgentAuthConfig(
448
442
  base_url=resolve_agent_auth_base_url(resolved),
449
443
  api_key=api_key,
450
444
  deployment_id=(resolved.get(DEPLOYMENT_ID_ENV) or "").strip() or None,
451
445
  workspace_id=workspace_id,
452
- service_key=service_key,
453
446
  )
454
447
 
455
448
 
@@ -190,9 +190,9 @@ def _read_user_connection_grants(
190
190
  raise CredentialStoreError(f"workspace connection {slug!r} is not configured")
191
191
  kind: Literal["oauth2", "secret"] = "secret" if row.kind == "secret" else "oauth2"
192
192
  token = (
193
- agent_auth.read_user_secret(slug)
193
+ agent_auth.read_user_secret_for_connection(row)
194
194
  if kind == "secret"
195
- else agent_auth.read_user_access_token(slug)
195
+ else agent_auth.read_user_access_token_for_connection(row)
196
196
  )
197
197
  grants.append(_UserConnectionGrant(slug=slug, kind=kind, token=token))
198
198
  return grants
@@ -124,6 +124,9 @@ CONNECTOR_EVENTS_PATH_SUFFIX = "/events"
124
124
  CHANNEL_EVENTS_USER = "mda:channel-events"
125
125
  CHANNEL_SERVICE_USER = "mda:channel"
126
126
 
127
+ #: Owner-key namespace for threads owned by a channel source conversation.
128
+ CHANNEL_THREAD_OWNER_PREFIX = "mda:source-thread:"
129
+
127
130
  _BUILTIN_CHANNEL_SOURCE_PROVIDERS = frozenset({"slack", "github"})
128
131
 
129
132
 
@@ -203,16 +206,20 @@ def build_managed_auth(identity: IdentityDefinitionLike) -> AuthInstance:
203
206
  return await _authenticate(cfg, http, auth_cls, headers, authorization, path, method)
204
207
 
205
208
  async def scope_threads(ctx: object, value: object) -> Mapping[str, str] | None:
206
- # Studio is deliberately not exempt from thread ownership. Studio
207
- # identity is reachable by any holder of a workspace LangSmith API key
208
- # (the Agent Server honours ``x-auth-scheme: langsmith``), so unscoping
209
- # it would let that key read and write every caller's threads. Under
210
- # ``mda dev`` the generated ``langgraph.json`` disables the Studio
211
- # scheme, so local Studio arrives as the local-dev service principal and
212
- # stays unscoped through the check below.
209
+ # Let Studio inspect every deployment thread so channel-triggered runs
210
+ # and their interrupts are visible there. Keep mutations owner-scoped:
211
+ # Studio identity is reachable by any holder of a workspace LangSmith
212
+ # API key, so exempting all actions would let that key alter caller
213
+ # threads too.
213
214
  user = getattr(ctx, "user", ctx)
214
215
  if is_local_dev_unscoped_user(user):
215
216
  return None
217
+ if (
218
+ is_studio_user is not None
219
+ and is_studio_user(user)
220
+ and getattr(ctx, "action", "") in {"read", "search"}
221
+ ):
222
+ return None
216
223
  key = _thread_owner_key(user)
217
224
  if isinstance(value, dict):
218
225
  payload = cast("JsonObject", value)
@@ -660,15 +667,50 @@ def _has_valid_ingress_secret(headers: Mapping[str, object]) -> bool:
660
667
  def _thread_owner_key(user: object) -> str:
661
668
  """Derive the owner value ``@auth.on.threads`` filters on.
662
669
 
670
+ A managed channel thread is owned by its source conversation; every other
671
+ caller owns its threads by user id. Channel threads are addressed by
672
+ ``source_thread_key`` and the deliveries that reach one can legitimately
673
+ carry different principals: the Trigger Server does not attach
674
+ ``identity_context`` to every envelope, and any participant may reply in a
675
+ shared thread or answer an interrupt someone else raised. Keying such a
676
+ thread on the delivering principal denies every later delivery that does
677
+ not match.
678
+
679
+ Only managed ingress sets both source stamps, and only over a trusted
680
+ loopback credential — a client presenting them is rejected in
681
+ :func:`_reject_untrusted_handoff_headers`. ``mda_user_id`` still carries the
682
+ principal, so credential resolution and store scoping stay per caller.
683
+
663
684
  Principals resolved by ``authenticate`` always carry ``mda_user_id``. A
664
685
  Studio user does not: the Agent Server builds it for
665
686
  ``x-auth-scheme: langsmith`` without consulting managed auth, so it exposes
666
687
  only ``identity``. Fall back to that rather than bucketing every such caller
667
688
  under an empty owner.
668
689
  """
690
+ conversation = _channel_conversation_owner_key(user)
691
+ if conversation is not None:
692
+ return conversation
669
693
  return str(_field(user, "mda_user_id") or _field(user, "identity") or "")
670
694
 
671
695
 
696
+ def _channel_conversation_owner_key(user: object) -> str | None:
697
+ """Owner key for a channel ingress caller, or ``None`` for anyone else.
698
+
699
+ The prefix keeps conversation owners in a namespace no ``mda_user_id`` can
700
+ reach (those are UUIDs, emails, or ``mda:*`` service ids), so a source
701
+ thread key can never collide with a caller.
702
+ """
703
+ provider = _trimmed_stamp(_field(user, "mda_source_provider"))
704
+ thread_key = _trimmed_stamp(_field(user, "mda_source_thread_id"))
705
+ if not provider or not thread_key:
706
+ return None
707
+ return f"{CHANNEL_THREAD_OWNER_PREFIX}{provider}:{thread_key}"
708
+
709
+
710
+ def _trimmed_stamp(value: object) -> str:
711
+ return value.strip() if isinstance(value, str) else ""
712
+
713
+
672
714
  #: LangGraph Store namespace prefix for Slack ↔ actor account links.
673
715
  IDENTITY_LINKS_STORE_PREFIX = "mda_identity_links"
674
716
 
@@ -19,7 +19,6 @@ 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
- CHANNEL_SERVICE_USER,
23
22
  HANDOFF_SECRET_ENV,
24
23
  HANDOFF_SECRET_HEADER,
25
24
  INGRESS_SECRET_ENV,
@@ -73,6 +72,7 @@ class ObservedIngressRun(TypedDict):
73
72
  class _IngressSource(TypedDict):
74
73
  provider: str
75
74
  thread_key: str
75
+ user_id: str
76
76
 
77
77
 
78
78
  class _ThreadsClient(Protocol):
@@ -165,8 +165,8 @@ async def ingress_source_thread_exists(
165
165
  ) -> bool:
166
166
  """True when an MDA thread already exists for this source-thread key.
167
167
 
168
- Call with the **resolved** actor (post identity remap) so ownership matches
169
- the thread created on the first run.
168
+ The thread is owned by the source conversation rather than by the caller, so
169
+ this answers "is the agent already in this thread" for any participant.
170
170
  """
171
171
  if not _has_loopback_credential():
172
172
  return False
@@ -236,10 +236,18 @@ async def start_ingress_agent(
236
236
  messages: list[dict[str, object]],
237
237
  source_provider: str,
238
238
  source_thread_key: str,
239
+ user_id: str,
239
240
  ) -> StartedIngressRun:
240
- """Create a Trigger-addressed thread and enqueue its run."""
241
+ """Create a Trigger-addressed thread and enqueue its run.
242
+
243
+ ``user_id`` is the Agent Auth principal UUID from Trigger ``identity_context``.
244
+ """
241
245
  client = _create_trigger_ingress_loopback_client(
242
- {"provider": source_provider, "thread_key": source_thread_key}
246
+ {
247
+ "provider": source_provider,
248
+ "thread_key": source_thread_key,
249
+ "user_id": user_id,
250
+ }
243
251
  )
244
252
  await client.threads.create(
245
253
  thread_id=thread_id,
@@ -268,10 +276,18 @@ async def resume_ingress_agent(
268
276
  resume: dict[str, object],
269
277
  source_provider: str,
270
278
  source_thread_key: str,
279
+ user_id: str,
271
280
  ) -> StartedIngressRun:
272
- """Resume a Trigger-addressed thread with a keyed interrupt response."""
281
+ """Resume a Trigger-addressed thread with a keyed interrupt response.
282
+
283
+ ``user_id`` is the Agent Auth principal UUID from Trigger ``identity_context``.
284
+ """
273
285
  client = _create_trigger_ingress_loopback_client(
274
- {"provider": source_provider, "thread_key": source_thread_key}
286
+ {
287
+ "provider": source_provider,
288
+ "thread_key": source_thread_key,
289
+ "user_id": user_id,
290
+ }
275
291
  )
276
292
  run = await client.runs.create(
277
293
  thread_id,
@@ -292,9 +308,18 @@ async def observe_ingress_agent(
292
308
  *,
293
309
  thread_id: str,
294
310
  run_id: str,
311
+ source_provider: str,
312
+ source_thread_key: str,
313
+ user_id: str,
295
314
  ) -> ObservedIngressRun:
296
- """Join an accepted run and read its terminal status."""
297
- client = _create_trigger_ingress_loopback_client()
315
+ """Join an accepted run as the principal that owns its thread."""
316
+ client = _create_trigger_ingress_loopback_client(
317
+ {
318
+ "provider": source_provider,
319
+ "thread_key": source_thread_key,
320
+ "user_id": user_id,
321
+ }
322
+ )
298
323
  values = await client.runs.join(thread_id, run_id)
299
324
  run = await client.runs.get(thread_id, run_id)
300
325
  status = run.get("status") if isinstance(run, Mapping) else None
@@ -312,7 +337,7 @@ async def observe_ingress_agent(
312
337
 
313
338
 
314
339
  def _create_trigger_ingress_loopback_client(
315
- source: _IngressSource | None = None,
340
+ source: _IngressSource,
316
341
  ) -> _IngressLoopbackClient:
317
342
  api_key = os.environ.get(LANGCHAIN_API_KEY_ENV, "").strip()
318
343
  if not api_key:
@@ -322,10 +347,17 @@ def _create_trigger_ingress_loopback_client(
322
347
  )
323
348
  module = import_module("langgraph_sdk")
324
349
  get_client = cast("_GetClient", module.get_client)
325
- headers = {USER_HEADER: CHANNEL_SERVICE_USER}
326
- if source is not None:
327
- headers[SOURCE_PROVIDER_HEADER] = source["provider"]
328
- headers[SOURCE_THREAD_HEADER] = source["thread_key"]
350
+ user_id = source["user_id"].strip()
351
+ if not user_id:
352
+ raise ChannelError(
353
+ "channel_run_failed",
354
+ "channel run requires an identity_context principal",
355
+ )
356
+ headers = {
357
+ USER_HEADER: user_id,
358
+ SOURCE_PROVIDER_HEADER: source["provider"],
359
+ SOURCE_THREAD_HEADER: source["thread_key"],
360
+ }
329
361
  return get_client(
330
362
  url=resolve_loopback_api_url(),
331
363
  api_key=api_key,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: managed-deepagents
3
- Version: 0.6.2.dev34
3
+ Version: 0.6.2.dev36
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
@@ -282,7 +282,9 @@ when a grant is missing. `mda dev` has no deployment to hold user grants, so
282
282
  a user-owned connection fails there with an actionable error; deploy the agent
283
283
  to exercise one.
284
284
 
285
- Provision workspace connections with `mda connections create` before deploy.
285
+ Deploy the project first, then provision its connections with
286
+ `mda connections create`. Agent-owned opaque secrets are scoped to that
287
+ deployment, so the command refuses to create one before `mda deploy`.
286
288
  `mda connections check` verifies manifest slugs exist in the workspace.
287
289
 
288
290
  Use `include_tools` or `exclude_tools` inside a server config to select a
@@ -1,20 +1,20 @@
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=3rA39j_3PFROrtsIr5OHkAO_FgAHfASM-ee0vGhPGu8,16650
4
- managed_deepagents/_agent_auth_http.py,sha256=DtspEFmWHTtpp3uqELw1-wuHiLbAx_ers9JW18J3Coc,3924
3
+ managed_deepagents/_agent_auth.py,sha256=bOxTT1ZX9BZZyvgIaxd9SZycbEaQl-HuCrPieCAv6G0,17258
4
+ managed_deepagents/_agent_auth_http.py,sha256=aCYn7Rrtw3cnK3r4ir4JWxwzFkb7cF7nBgPpNdydxDw,5108
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=N6luj1YMpBPjpH-WnGc6AIGNTE9DnZoBWAlWcFRSo2I,16518
7
+ managed_deepagents/_connections.py,sha256=VwVBwM4T0rp1MgYu9D8QjGj1ireYeEuP3VIAFN5urFY,16246
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=WlryhbWHtg1FN5IdadiOvgT3JX3G78DtWVGW0wLWvz4,12252
10
+ managed_deepagents/_credential_gate.py,sha256=SNxXunqmHgjBsZpiZu8yp49YkRIO0amkl0-7GHLGnIg,12280
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=BydhEhqxkb9mUg04hBZlGeyxzlKAogrMfu-r4X_bzdU,30983
13
+ managed_deepagents/_identity_auth.py,sha256=l0t0Xx-J1-z_Zw4K5D-B2yV6lNdeJgOw7UvxODxUxZQ,32798
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
16
  managed_deepagents/_identity_runtime.py,sha256=vsshj0TNWd-rXUhVsPAqXIkANunQT7jI94CWgzq-1cY,24409
17
- managed_deepagents/_ingress.py,sha256=mPIb1o60G_pCg4Qotwt-SUloCxY1xXfUTSyN_BPZNts,11581
17
+ managed_deepagents/_ingress.py,sha256=iqsGm2jZKKn0FqjNn2nJAlR7d-IqCpVKWo6NbMEhjxw,12323
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
@@ -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=XsmY2FZEeZ4zFKjZ2uAL0VWIbYEJYKu66n6rKEihWuE,3855872
31
+ managed_deepagents/_binaries/mda.exe,sha256=beXeCdpeh0KOXNrpeg-iudNWEv54xwLyOp0XVkL6MyA,3855360
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
@@ -50,8 +50,9 @@ managed_deepagents/_channels/slack/transport.py,sha256=_xQmzK2hBpPOoY3zKFhzc3vvl
50
50
  managed_deepagents/_channels/trigger/__init__.py,sha256=bWj66XoPbDdrztPUCKBuSJBBS2pBocn4EA8FDE3WeBA,48
51
51
  managed_deepagents/_channels/trigger/channel.py,sha256=rKSvOABOZX58ZIvZ3hKcYxvTx9npQ0YXPQWk9JEy6XE,5108
52
52
  managed_deepagents/_channels/trigger/client.py,sha256=cM97gzYWUTFC9o3uoveFe9-HxZc6_VqiqC8u4fKqjXc,8572
53
- managed_deepagents/_channels/trigger/http.py,sha256=L5ILylDH0BXO5MAfHpXGF742vfCxnRhGLNbLn5kQUCY,12303
54
- managed_deepagents/_channels/trigger/types.py,sha256=G4kW1zKV2JlNyV7evGra-lSrZ_ZpVsRQm2iSx0iLEGE,6277
53
+ managed_deepagents/_channels/trigger/http.py,sha256=O8yMleioXj3s5QXj7jK-HpUpiSFppjfRjHJ2tdfX0Is,16205
54
+ managed_deepagents/_channels/trigger/identity_context.py,sha256=oo6sgtNDLGGkZCzrkrifMHkkAqxj7ui2HgUUdjNA1iU,7701
55
+ managed_deepagents/_channels/trigger/types.py,sha256=42JMOFXP9MfqwQ66SFlAiwC7q_AnNIJXBts3sAujxac,6625
55
56
  managed_deepagents/_connectors/__init__.py,sha256=c_SdH3T7T6bgX7-I5jxAqDzHojmqVUCX0AXRI7g5Ovs,575
56
57
  managed_deepagents/_connectors/tool_selection.py,sha256=xMvohEvg_rguQvziUXzxKn4V140mmearZHel97F8tVs,2978
57
58
  managed_deepagents/_connectors/mcp/__init__.py,sha256=3AL429g5djYEYuiuSqsxj2VESpTkyU2NeBIJsHlWVk4,11162
@@ -68,9 +69,9 @@ managed_deepagents/identity/__init__.py,sha256=Slm-oEqKxbaZEQsMqHfD3LbG5M4vH2lKU
68
69
  managed_deepagents/identity/claims.py,sha256=prby7r_8Lww3MmoBm8PX6PsSGE3GwSHkr8mVIz_08nU,1180
69
70
  managed_deepagents/identity/options.py,sha256=x_yMbXEFR7zXyehEjDHG2qjAlukXjrRBcQLcM6IndLM,2550
70
71
  managed_deepagents/identity/supabase.py,sha256=8uobl3WCrQL_ieSXG8KTLf1th7Qjim9liZT50HeaJE0,6026
71
- managed_deepagents-0.6.2.dev34.dist-info/licenses/LICENSE,sha256=WnPZQQ608ojZUkXl7BW6VbKloIAibZiGm1K6cjeZOL8,1066
72
- managed_deepagents-0.6.2.dev34.dist-info/METADATA,sha256=WTv8_E_fBXtTODOPUqUrNf0qcaK01ym-dPnhOonzF3E,13582
73
- managed_deepagents-0.6.2.dev34.dist-info/WHEEL,sha256=3I5VVWZdsFlU417aCS2bMRuerS8wsfvBq99MyatBV3A,97
74
- managed_deepagents-0.6.2.dev34.dist-info/entry_points.txt,sha256=ziZqkKuaDO3qHCjKmBhrx9cm3ok8bZ9xcEDpUTj4unM,53
75
- managed_deepagents-0.6.2.dev34.dist-info/top_level.txt,sha256=fdBo4O0Hlol_w022tTLB5ftZpamb4yJoQ3MUvjgTGfA,19
76
- managed_deepagents-0.6.2.dev34.dist-info/RECORD,,
72
+ managed_deepagents-0.6.2.dev36.dist-info/licenses/LICENSE,sha256=WnPZQQ608ojZUkXl7BW6VbKloIAibZiGm1K6cjeZOL8,1066
73
+ managed_deepagents-0.6.2.dev36.dist-info/METADATA,sha256=TcDnuQedflv-AcffJe1UoQkL5yvtwtfI4V6gfMrnqag,13709
74
+ managed_deepagents-0.6.2.dev36.dist-info/WHEEL,sha256=3I5VVWZdsFlU417aCS2bMRuerS8wsfvBq99MyatBV3A,97
75
+ managed_deepagents-0.6.2.dev36.dist-info/entry_points.txt,sha256=ziZqkKuaDO3qHCjKmBhrx9cm3ok8bZ9xcEDpUTj4unM,53
76
+ managed_deepagents-0.6.2.dev36.dist-info/top_level.txt,sha256=fdBo4O0Hlol_w022tTLB5ftZpamb4yJoQ3MUvjgTGfA,19
77
+ managed_deepagents-0.6.2.dev36.dist-info/RECORD,,