managed-deepagents 0.6.2.dev36__py3-none-win_amd64.whl → 0.6.2.dev37__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,7 +23,6 @@ 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
27
26
  _HTTP_NOT_FOUND = 404
28
27
  _HTTP_CONFLICT = 409
29
28
 
@@ -47,6 +46,8 @@ class AgentAuthContext:
47
46
  principal_id: str
48
47
  api_key: str | None = None
49
48
  workspace_id: str | None = None
49
+ #: Agent Builder service JWT for workspace-owned credential material.
50
+ service_key: str | None = None
50
51
 
51
52
 
52
53
  class PendingAuthorization(TypedDict):
@@ -133,7 +134,7 @@ class AgentAuth:
133
134
  f"/v1/agent-auth/connections?{urllib.parse.urlencode(query)}",
134
135
  )
135
136
  if status >= _HTTP_BAD_REQUEST:
136
- raise CredentialStoreError("Agent Auth list connections failed", status, body)
137
+ raise CredentialStoreError("Agent Auth list connections failed", status)
137
138
  raw_items = body.get("items")
138
139
  items = raw_items if isinstance(raw_items, list) else []
139
140
  for item in items:
@@ -169,17 +170,28 @@ class AgentAuth:
169
170
  def find_secret_credential_id(self, connection: WorkspaceConnection, slug: str) -> str | None:
170
171
  """Resolve the credential id for a secret connection row.
171
172
 
172
- Agent-owned connection declarations scope the credential to this
173
- deployment. Workspace-owned credentials are deliberately not visible
174
- to deployments.
173
+ With an Agent Builder service key, prefer GET-for-owner / workspace list.
174
+ Personal API keys may only list agent-owned credentials.
175
175
  """
176
176
  if connection.kind != "secret":
177
177
  return None
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
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
+ )
183
195
  return self.find_credential_id(
184
196
  owner_type="agent",
185
197
  owner_id=self.context.agent_id,
@@ -190,12 +202,6 @@ class AgentAuth:
190
202
  def _credential_id_for_owner(
191
203
  self, connection_id: str, owner_type: str, owner_id: str
192
204
  ) -> 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
- """
199
205
  query = urllib.parse.urlencode(
200
206
  {
201
207
  "credential_owner_type": owner_type,
@@ -209,27 +215,16 @@ class AgentAuth:
209
215
  if status == _HTTP_NOT_FOUND:
210
216
  return None
211
217
  if status >= _HTTP_BAD_REQUEST:
212
- raise CredentialStoreError(
213
- "Agent Auth get connection for owner failed"
214
- + (" (forbidden)" if status == _HTTP_FORBIDDEN else ""),
215
- status,
216
- body,
217
- )
218
+ raise CredentialStoreError("Agent Auth get connection for owner failed", status)
218
219
  return _connection_credential_id(body)
219
220
 
220
221
  def read_user_secret(self, slug: str) -> str | None:
221
222
  """Read a caller-owned opaque secret grant for a workspace connection 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
223
+ credential_id = self.find_credential_id(
224
+ owner_type="user",
225
+ owner_id=self.context.principal_id,
226
+ kind="secret",
227
+ slug=slug,
233
228
  )
234
229
  if credential_id is None:
235
230
  return None
@@ -249,17 +244,11 @@ class AgentAuth:
249
244
  Returns:
250
245
  The access token, or ``None`` when the caller must connect.
251
246
  """
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
247
+ credential_id = self.find_credential_id(
248
+ owner_type="user",
249
+ owner_id=self.context.principal_id,
250
+ kind="oauth2",
251
+ slug=slug,
263
252
  )
264
253
  if credential_id is None:
265
254
  return None
@@ -283,7 +272,7 @@ class AgentAuth:
283
272
  )
284
273
  if status >= _HTTP_BAD_REQUEST:
285
274
  raise CredentialStoreError(
286
- "Agent Auth create oauth-authorization-session failed", status, body
275
+ "Agent Auth create oauth-authorization-session failed", status
287
276
  )
288
277
  auth_id = str(body.get("id") or "")
289
278
  connect_url = str(body.get("verification_url") or "")
@@ -304,7 +293,7 @@ class AgentAuth:
304
293
  "GET", f"/v1/agent-auth/oauth-providers?{urllib.parse.urlencode(query)}"
305
294
  )
306
295
  if status >= _HTTP_BAD_REQUEST:
307
- raise CredentialStoreError("Agent Auth list oauth-providers failed", status, body)
296
+ raise CredentialStoreError("Agent Auth list oauth-providers failed", status)
308
297
  raw_items = body.get("items")
309
298
  for item in raw_items if isinstance(raw_items, list) else []:
310
299
  if isinstance(item, Mapping) and item.get("slug") == slug and item.get("id"):
@@ -340,7 +329,7 @@ class AgentAuth:
340
329
  )
341
330
  status, body = self._request("GET", f"/v1/agent-auth/credentials?{query}")
342
331
  if status >= _HTTP_BAD_REQUEST:
343
- raise CredentialStoreError("Agent Auth list credentials failed", status, body)
332
+ raise CredentialStoreError("Agent Auth list credentials failed", status)
344
333
  raw_items = body.get("items")
345
334
  items = raw_items if isinstance(raw_items, list) else []
346
335
  for item in items:
@@ -363,7 +352,7 @@ class AgentAuth:
363
352
  if status == _HTTP_NOT_FOUND:
364
353
  return None
365
354
  if status >= _HTTP_BAD_REQUEST:
366
- raise CredentialStoreError("Agent Auth get secret failed", status, body)
355
+ raise CredentialStoreError("Agent Auth get secret failed", status)
367
356
  secret = body.get("secret")
368
357
  return secret if isinstance(secret, str) and secret else None
369
358
 
@@ -375,7 +364,7 @@ class AgentAuth:
375
364
  if _is_authorization_required(status, body):
376
365
  return None
377
366
  if status >= _HTTP_BAD_REQUEST:
378
- raise CredentialStoreError("Agent Auth get oauth2 failed", status, body)
367
+ raise CredentialStoreError("Agent Auth get oauth2 failed", status)
379
368
  oauth2 = body.get("oauth2") if isinstance(body.get("oauth2"), dict) else body
380
369
  token = oauth2.get("access_token") if isinstance(oauth2, dict) else None
381
370
  return token if isinstance(token, str) and token else None
@@ -401,6 +390,7 @@ class AgentAuth:
401
390
  return agent_auth_headers(
402
391
  api_key=self.context.api_key,
403
392
  workspace_id=self.context.workspace_id,
393
+ service_key=self.context.service_key,
404
394
  )
405
395
 
406
396
 
@@ -11,68 +11,32 @@ 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, Final
14
+ from typing import TYPE_CHECKING
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
47
21
 
48
22
 
49
23
  class CredentialStoreError(RuntimeError):
50
24
  """Agent Auth could not answer.
51
25
 
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.
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.
55
28
  """
56
29
 
57
- def __init__(
58
- self,
59
- message: str,
60
- status: int | None = None,
61
- problem: Mapping[str, object] | None = None,
62
- ) -> None:
30
+ def __init__(self, message: str, status: int | None = None) -> None:
63
31
  self.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}")
32
+ super().__init__(message if status is None else f"{message} (status {status})")
67
33
 
68
34
 
69
35
  __all__ = [
70
- "AGENT_AUTH_PROBLEM_HINT_MAX_CHARS",
71
36
  "AGENT_AUTH_TIMEOUT_SECONDS",
72
37
  "CredentialStoreError",
73
38
  "agent_auth_credential_query",
74
39
  "agent_auth_headers",
75
- "agent_auth_problem_hint",
76
40
  "request_agent_auth_json",
77
41
  ]
78
42
 
@@ -106,12 +70,16 @@ def agent_auth_headers(
106
70
  *,
107
71
  api_key: str | None = None,
108
72
  workspace_id: str | None = None,
73
+ service_key: str | None = None,
109
74
  ) -> dict[str, str]:
110
75
  headers = {
111
76
  "accept": "application/json",
112
77
  "content-type": "application/json",
113
78
  "x-langsmith-product": "mda",
114
79
  }
80
+ resolved_service_key = (service_key or "").strip()
81
+ if resolved_service_key:
82
+ headers["x-service-key"] = resolved_service_key
115
83
  resolved_api_key = (api_key or "").strip()
116
84
  if resolved_api_key:
117
85
  headers["x-api-key"] = resolved_api_key
Binary file
@@ -16,12 +16,6 @@ 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
- )
25
19
  from managed_deepagents._channels.trigger.types import (
26
20
  TRIGGER_SERVICE_KEY_HEADER,
27
21
  TriggerChannelEvent,
@@ -58,7 +52,6 @@ class StartAgentFn(Protocol):
58
52
  messages: list[dict[str, object]],
59
53
  source_provider: str,
60
54
  source_thread_key: str,
61
- user_id: str,
62
55
  ) -> StartedIngressRun: ...
63
56
 
64
57
 
@@ -71,7 +64,6 @@ class ResumeAgentFn(Protocol):
71
64
  resume: TriggerChannelResume,
72
65
  source_provider: str,
73
66
  source_thread_key: str,
74
- user_id: str,
75
67
  ) -> StartedIngressRun: ...
76
68
 
77
69
 
@@ -82,7 +74,6 @@ async def _start_event_run(
82
74
  source_provider: str,
83
75
  start_agent: StartAgentFn | None,
84
76
  resume_agent: ResumeAgentFn | None,
85
- user_id: str,
86
77
  ) -> StartedIngressRun:
87
78
  thread_id = _thread_id(event["source_thread_key"])
88
79
  if event["type"] == "message":
@@ -93,7 +84,6 @@ async def _start_event_run(
93
84
  messages=event["input"]["messages"],
94
85
  source_provider=source_provider,
95
86
  source_thread_key=event["source_thread_key"],
96
- user_id=user_id,
97
87
  )
98
88
  resume = resume_agent or resume_ingress_agent
99
89
  return await resume(
@@ -102,11 +92,10 @@ async def _start_event_run(
102
92
  resume=event["interaction"]["resume"],
103
93
  source_provider=source_provider,
104
94
  source_thread_key=event["source_thread_key"],
105
- user_id=user_id,
106
95
  )
107
96
 
108
97
 
109
- async def handle_trigger_channel_event_request( # noqa: C901, PLR0911
98
+ async def handle_trigger_channel_event_request(
110
99
  request: object,
111
100
  *,
112
101
  source_provider: str,
@@ -117,11 +106,9 @@ async def handle_trigger_channel_event_request( # noqa: C901, PLR0911
117
106
  resume_agent: ResumeAgentFn | None = None,
118
107
  observe_agent: ObserveAgentFn | None = None,
119
108
  trigger_client: TriggerClientLike | None = None,
120
- identity_context_key: IdentityContextKey | None = None,
121
- identity_context_issuer: str | None = None,
122
109
  ) -> object:
123
110
  """Validate a Trigger message and enqueue its Agent Server run."""
124
- # TODO: Validate Trigger's platform-signed x-service-key JWT (not presence).
111
+ # TODO: Validate a per-deployment Trigger↔MDA shared secret.
125
112
  if not _nonblank_header(request, TRIGGER_SERVICE_KEY_HEADER):
126
113
  return _json_response({"error": "unauthorized"}, status_code=401)
127
114
 
@@ -130,32 +117,6 @@ async def handle_trigger_channel_event_request( # noqa: C901, PLR0911
130
117
  except (TypeError, ValueError, UnicodeDecodeError):
131
118
  logger.warning("invalid channel event")
132
119
  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
-
159
120
  logger.info(
160
121
  "Channel incoming event (type=%s, event_id=%s, delivery_id=%s, channel_id=%s)",
161
122
  event["type"],
@@ -198,7 +159,6 @@ async def handle_trigger_channel_event_request( # noqa: C901, PLR0911
198
159
  source_provider=source_provider,
199
160
  start_agent=start_agent,
200
161
  resume_agent=resume_agent,
201
- user_id=user_id,
202
162
  )
203
163
  except Exception as error: # noqa: BLE001 - never expose Agent Server details in the response
204
164
  logger.error(
@@ -213,9 +173,6 @@ async def handle_trigger_channel_event_request( # noqa: C901, PLR0911
213
173
  started=started,
214
174
  client=client,
215
175
  observe_agent=observe_agent,
216
- source_provider=source_provider,
217
- source_thread_key=event["source_thread_key"],
218
- user_id=user_id,
219
176
  )
220
177
 
221
178
  schedule_ingress_work(
@@ -235,9 +192,6 @@ async def _observe_accepted_run(
235
192
  started: StartedIngressRun,
236
193
  client: TriggerClientLike,
237
194
  observe_agent: ObserveAgentFn | None,
238
- source_provider: str | None = None,
239
- source_thread_key: str | None = None,
240
- user_id: str,
241
195
  ) -> None:
242
196
  async def lifecycle(
243
197
  event_type: TriggerChannelRuntimeEventType,
@@ -258,13 +212,11 @@ async def _observe_accepted_run(
258
212
  )
259
213
 
260
214
  await lifecycle("run.started")
215
+ observe = observe_agent or observe_ingress_agent
261
216
  try:
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,
217
+ observed = await observe(
218
+ thread_id=started["thread_id"],
219
+ run_id=started["run_id"],
268
220
  )
269
221
  except Exception: # noqa: BLE001 - observation details stay internal
270
222
  await lifecycle("run.failed")
@@ -311,53 +263,6 @@ async def _observe_accepted_run(
311
263
  await lifecycle("run.completed")
312
264
 
313
265
 
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
-
361
266
  def _is_interrupted_run(observed: ObservedIngressRun) -> bool:
362
267
  # Agent Server can report status "success" after join while values still
363
268
  # carry `__interrupt__`. Trust either signal.
@@ -410,27 +315,6 @@ def _thread_id(source_thread_key: str) -> str:
410
315
  return str(uuid.uuid5(uuid.NAMESPACE_URL, source_thread_key))
411
316
 
412
317
 
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
-
434
318
  def _resolve_assistant_id(override: str | None) -> str:
435
319
  if override is not None and (explicit := override.strip()):
436
320
  return explicit
@@ -25,7 +25,7 @@ class TriggerChannelMessageInput(TypedDict):
25
25
  messages: list[dict[str, object]]
26
26
 
27
27
 
28
- class TriggerChannelMessageEventRequired(TypedDict):
28
+ class TriggerChannelMessageEvent(TypedDict):
29
29
  version: Literal["1"]
30
30
  type: Literal["message"]
31
31
  event_id: str
@@ -37,10 +37,6 @@ class TriggerChannelMessageEventRequired(TypedDict):
37
37
  input: TriggerChannelMessageInput
38
38
 
39
39
 
40
- class TriggerChannelMessageEvent(TriggerChannelMessageEventRequired, total=False):
41
- identity_context: str | None
42
-
43
-
44
40
  # Keyed LangGraph resume: `{interrupt_id: value}`. Values are opaque
45
41
  # (`interrupt()` results or HITL `{decisions}`).
46
42
  TriggerChannelResume: TypeAlias = dict[str, object]
@@ -51,7 +47,7 @@ class TriggerChannelInterruptResumeInteraction(TypedDict):
51
47
  resume: TriggerChannelResume
52
48
 
53
49
 
54
- class TriggerChannelInterruptResumeEventRequired(TypedDict):
50
+ class TriggerChannelInterruptResumeEvent(TypedDict):
55
51
  version: Literal["1"]
56
52
  type: Literal["interrupt_resume"]
57
53
  event_id: str
@@ -63,10 +59,6 @@ class TriggerChannelInterruptResumeEventRequired(TypedDict):
63
59
  interaction: TriggerChannelInterruptResumeInteraction
64
60
 
65
61
 
66
- class TriggerChannelInterruptResumeEvent(TriggerChannelInterruptResumeEventRequired, total=False):
67
- identity_context: str | None
68
-
69
-
70
62
  TriggerChannelEvent = TriggerChannelMessageEvent | TriggerChannelInterruptResumeEvent
71
63
 
72
64
 
@@ -178,7 +170,6 @@ class _MessageEventModel(_WireModel):
178
170
  source_thread_key: _NonBlankString
179
171
  address: SkipValidation[dict[str, object]]
180
172
  input: _InputModel
181
- identity_context: str | None = None
182
173
 
183
174
  @field_validator("address", mode="before")
184
175
  @classmethod
@@ -206,7 +197,6 @@ class _InterruptResumeEventModel(_WireModel):
206
197
  source_thread_key: _NonBlankString
207
198
  address: SkipValidation[dict[str, object]]
208
199
  interaction: _InterruptResumeInteractionModel
209
- identity_context: str | None = None
210
200
 
211
201
  @field_validator("address", mode="before")
212
202
  @classmethod
@@ -1,6 +1,6 @@
1
1
  """``connections.get(...)`` — read a managed connection from a tool.
2
2
 
3
- ``{ type: "agent" }`` resolves material owned by the current deployment.
3
+ ``{ type: "agent" }`` resolves shared workspace material on the connection row.
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,10 +157,11 @@ class _AgentAuthConfig:
157
157
  api_key: str
158
158
  deployment_id: str | None
159
159
  workspace_id: str
160
+ service_key: str | None = None
160
161
 
161
162
 
162
163
  class AgentAuthConnectionStore:
163
- """Store backed by connection rows and deployment/user grants in Agent Auth."""
164
+ """Store backed by workspace connection rows in Agent Auth."""
164
165
 
165
166
  def __init__(
166
167
  self,
@@ -187,6 +188,7 @@ class AgentAuthConnectionStore:
187
188
  principal_id=owner_id,
188
189
  api_key=config.api_key,
189
190
  workspace_id=config.workspace_id,
191
+ service_key=config.service_key,
190
192
  ),
191
193
  request=self._bind_request(config),
192
194
  timeout=self._timeout,
@@ -213,6 +215,7 @@ class AgentAuthConnectionStore:
213
215
  headers = agent_auth_headers(
214
216
  api_key=config.api_key,
215
217
  workspace_id=config.workspace_id,
218
+ service_key=config.service_key,
216
219
  )
217
220
  response = http_get(url, headers, timeout)
218
221
  status = response.get("status")
@@ -251,18 +254,19 @@ def _lookup_workspace_connection(
251
254
  if row is None:
252
255
  return None
253
256
  if owner["type"] == "user":
254
- return _lookup_user_connection_material(client, row)
257
+ return _lookup_user_connection_material(client, slug, row)
255
258
  return _lookup_agent_connection_material(client, slug, row)
256
259
 
257
260
 
258
261
  def _lookup_user_connection_material(
259
262
  client: AgentAuth,
263
+ slug: str,
260
264
  row: WorkspaceConnection,
261
265
  ) -> str | None:
262
266
  if row.kind == "secret":
263
- return client.read_user_secret_for_connection(row)
267
+ return client.read_user_secret(slug)
264
268
  if row.kind == "oauth2":
265
- return client.read_user_access_token_for_connection(row)
269
+ return client.read_user_access_token(slug)
266
270
  return None
267
271
 
268
272
 
@@ -425,6 +429,7 @@ def resolve_agent_auth_context(
425
429
  principal_id=resolve_owner_id({"type": "user"}, identity, config.deployment_id),
426
430
  api_key=config.api_key,
427
431
  workspace_id=config.workspace_id,
432
+ service_key=config.service_key,
428
433
  )
429
434
 
430
435
 
@@ -438,11 +443,13 @@ def _resolve_agent_auth_config(env: Mapping[str, str] | None) -> _AgentAuthConfi
438
443
  raise CredentialStoreError(
439
444
  f"connections are not configured: {_WORKSPACE_ID_ENV} is not set"
440
445
  )
446
+ service_key = (resolved.get("MDA_AGENT_AUTH_SERVICE_KEY") or "").strip() or None
441
447
  return _AgentAuthConfig(
442
448
  base_url=resolve_agent_auth_base_url(resolved),
443
449
  api_key=api_key,
444
450
  deployment_id=(resolved.get(DEPLOYMENT_ID_ENV) or "").strip() or None,
445
451
  workspace_id=workspace_id,
452
+ service_key=service_key,
446
453
  )
447
454
 
448
455
 
@@ -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_for_connection(row)
193
+ agent_auth.read_user_secret(slug)
194
194
  if kind == "secret"
195
- else agent_auth.read_user_access_token_for_connection(row)
195
+ else agent_auth.read_user_access_token(slug)
196
196
  )
197
197
  grants.append(_UserConnectionGrant(slug=slug, kind=kind, token=token))
198
198
  return grants
@@ -124,9 +124,6 @@ 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
-
130
127
  _BUILTIN_CHANNEL_SOURCE_PROVIDERS = frozenset({"slack", "github"})
131
128
 
132
129
 
@@ -206,20 +203,16 @@ def build_managed_auth(identity: IdentityDefinitionLike) -> AuthInstance:
206
203
  return await _authenticate(cfg, http, auth_cls, headers, authorization, path, method)
207
204
 
208
205
  async def scope_threads(ctx: object, value: object) -> Mapping[str, str] | None:
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.
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.
214
213
  user = getattr(ctx, "user", ctx)
215
214
  if is_local_dev_unscoped_user(user):
216
215
  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
223
216
  key = _thread_owner_key(user)
224
217
  if isinstance(value, dict):
225
218
  payload = cast("JsonObject", value)
@@ -667,50 +660,15 @@ def _has_valid_ingress_secret(headers: Mapping[str, object]) -> bool:
667
660
  def _thread_owner_key(user: object) -> str:
668
661
  """Derive the owner value ``@auth.on.threads`` filters on.
669
662
 
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
-
684
663
  Principals resolved by ``authenticate`` always carry ``mda_user_id``. A
685
664
  Studio user does not: the Agent Server builds it for
686
665
  ``x-auth-scheme: langsmith`` without consulting managed auth, so it exposes
687
666
  only ``identity``. Fall back to that rather than bucketing every such caller
688
667
  under an empty owner.
689
668
  """
690
- conversation = _channel_conversation_owner_key(user)
691
- if conversation is not None:
692
- return conversation
693
669
  return str(_field(user, "mda_user_id") or _field(user, "identity") or "")
694
670
 
695
671
 
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
-
714
672
  #: LangGraph Store namespace prefix for Slack ↔ actor account links.
715
673
  IDENTITY_LINKS_STORE_PREFIX = "mda_identity_links"
716
674
 
@@ -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
+ CHANNEL_SERVICE_USER,
22
23
  HANDOFF_SECRET_ENV,
23
24
  HANDOFF_SECRET_HEADER,
24
25
  INGRESS_SECRET_ENV,
@@ -72,7 +73,6 @@ class ObservedIngressRun(TypedDict):
72
73
  class _IngressSource(TypedDict):
73
74
  provider: str
74
75
  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
- 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.
168
+ Call with the **resolved** actor (post identity remap) so ownership matches
169
+ the thread created on the first run.
170
170
  """
171
171
  if not _has_loopback_credential():
172
172
  return False
@@ -236,18 +236,10 @@ 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,
240
239
  ) -> StartedIngressRun:
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
- """
240
+ """Create a Trigger-addressed thread and enqueue its run."""
245
241
  client = _create_trigger_ingress_loopback_client(
246
- {
247
- "provider": source_provider,
248
- "thread_key": source_thread_key,
249
- "user_id": user_id,
250
- }
242
+ {"provider": source_provider, "thread_key": source_thread_key}
251
243
  )
252
244
  await client.threads.create(
253
245
  thread_id=thread_id,
@@ -276,18 +268,10 @@ async def resume_ingress_agent(
276
268
  resume: dict[str, object],
277
269
  source_provider: str,
278
270
  source_thread_key: str,
279
- user_id: str,
280
271
  ) -> StartedIngressRun:
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
- """
272
+ """Resume a Trigger-addressed thread with a keyed interrupt response."""
285
273
  client = _create_trigger_ingress_loopback_client(
286
- {
287
- "provider": source_provider,
288
- "thread_key": source_thread_key,
289
- "user_id": user_id,
290
- }
274
+ {"provider": source_provider, "thread_key": source_thread_key}
291
275
  )
292
276
  run = await client.runs.create(
293
277
  thread_id,
@@ -308,18 +292,9 @@ async def observe_ingress_agent(
308
292
  *,
309
293
  thread_id: str,
310
294
  run_id: str,
311
- source_provider: str,
312
- source_thread_key: str,
313
- user_id: str,
314
295
  ) -> ObservedIngressRun:
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
- )
296
+ """Join an accepted run and read its terminal status."""
297
+ client = _create_trigger_ingress_loopback_client()
323
298
  values = await client.runs.join(thread_id, run_id)
324
299
  run = await client.runs.get(thread_id, run_id)
325
300
  status = run.get("status") if isinstance(run, Mapping) else None
@@ -337,7 +312,7 @@ async def observe_ingress_agent(
337
312
 
338
313
 
339
314
  def _create_trigger_ingress_loopback_client(
340
- source: _IngressSource,
315
+ source: _IngressSource | None = None,
341
316
  ) -> _IngressLoopbackClient:
342
317
  api_key = os.environ.get(LANGCHAIN_API_KEY_ENV, "").strip()
343
318
  if not api_key:
@@ -347,17 +322,10 @@ def _create_trigger_ingress_loopback_client(
347
322
  )
348
323
  module = import_module("langgraph_sdk")
349
324
  get_client = cast("_GetClient", module.get_client)
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
- }
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"]
361
329
  return get_client(
362
330
  url=resolve_loopback_api_url(),
363
331
  api_key=api_key,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: managed-deepagents
3
- Version: 0.6.2.dev36
3
+ Version: 0.6.2.dev37
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,9 +282,7 @@ 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
- 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`.
285
+ Provision workspace connections with `mda connections create` before deploy.
288
286
  `mda connections check` verifies manifest slugs exist in the workspace.
289
287
 
290
288
  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=bOxTT1ZX9BZZyvgIaxd9SZycbEaQl-HuCrPieCAv6G0,17258
4
- managed_deepagents/_agent_auth_http.py,sha256=aCYn7Rrtw3cnK3r4ir4JWxwzFkb7cF7nBgPpNdydxDw,5108
3
+ managed_deepagents/_agent_auth.py,sha256=3rA39j_3PFROrtsIr5OHkAO_FgAHfASM-ee0vGhPGu8,16650
4
+ managed_deepagents/_agent_auth_http.py,sha256=DtspEFmWHTtpp3uqELw1-wuHiLbAx_ers9JW18J3Coc,3924
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=N6luj1YMpBPjpH-WnGc6AIGNTE9DnZoBWAlWcFRSo2I,16518
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=SNxXunqmHgjBsZpiZu8yp49YkRIO0amkl0-7GHLGnIg,12280
10
+ managed_deepagents/_credential_gate.py,sha256=WlryhbWHtg1FN5IdadiOvgT3JX3G78DtWVGW0wLWvz4,12252
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=BydhEhqxkb9mUg04hBZlGeyxzlKAogrMfu-r4X_bzdU,30983
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=iqsGm2jZKKn0FqjNn2nJAlR7d-IqCpVKWo6NbMEhjxw,12323
17
+ managed_deepagents/_ingress.py,sha256=mPIb1o60G_pCg4Qotwt-SUloCxY1xXfUTSyN_BPZNts,11581
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=beXeCdpeh0KOXNrpeg-iudNWEv54xwLyOp0XVkL6MyA,3855360
31
+ managed_deepagents/_binaries/mda.exe,sha256=riwU4PKA6DzRCX00K8ObfnFNO2lSlx_Zx1Lj9yOCe3c,3871744
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,9 +50,8 @@ 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=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
53
+ managed_deepagents/_channels/trigger/http.py,sha256=L5ILylDH0BXO5MAfHpXGF742vfCxnRhGLNbLn5kQUCY,12303
54
+ managed_deepagents/_channels/trigger/types.py,sha256=G4kW1zKV2JlNyV7evGra-lSrZ_ZpVsRQm2iSx0iLEGE,6277
56
55
  managed_deepagents/_connectors/__init__.py,sha256=c_SdH3T7T6bgX7-I5jxAqDzHojmqVUCX0AXRI7g5Ovs,575
57
56
  managed_deepagents/_connectors/tool_selection.py,sha256=xMvohEvg_rguQvziUXzxKn4V140mmearZHel97F8tVs,2978
58
57
  managed_deepagents/_connectors/mcp/__init__.py,sha256=3AL429g5djYEYuiuSqsxj2VESpTkyU2NeBIJsHlWVk4,11162
@@ -69,9 +68,9 @@ managed_deepagents/identity/__init__.py,sha256=Slm-oEqKxbaZEQsMqHfD3LbG5M4vH2lKU
69
68
  managed_deepagents/identity/claims.py,sha256=prby7r_8Lww3MmoBm8PX6PsSGE3GwSHkr8mVIz_08nU,1180
70
69
  managed_deepagents/identity/options.py,sha256=x_yMbXEFR7zXyehEjDHG2qjAlukXjrRBcQLcM6IndLM,2550
71
70
  managed_deepagents/identity/supabase.py,sha256=8uobl3WCrQL_ieSXG8KTLf1th7Qjim9liZT50HeaJE0,6026
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,,
71
+ managed_deepagents-0.6.2.dev37.dist-info/licenses/LICENSE,sha256=WnPZQQ608ojZUkXl7BW6VbKloIAibZiGm1K6cjeZOL8,1066
72
+ managed_deepagents-0.6.2.dev37.dist-info/METADATA,sha256=iOVDEIhlqrDnlDJbX7MW9LrTY1u0ExCd_4V0dWCHP38,13582
73
+ managed_deepagents-0.6.2.dev37.dist-info/WHEEL,sha256=3I5VVWZdsFlU417aCS2bMRuerS8wsfvBq99MyatBV3A,97
74
+ managed_deepagents-0.6.2.dev37.dist-info/entry_points.txt,sha256=ziZqkKuaDO3qHCjKmBhrx9cm3ok8bZ9xcEDpUTj4unM,53
75
+ managed_deepagents-0.6.2.dev37.dist-info/top_level.txt,sha256=fdBo4O0Hlol_w022tTLB5ftZpamb4yJoQ3MUvjgTGfA,19
76
+ managed_deepagents-0.6.2.dev37.dist-info/RECORD,,
@@ -1,214 +0,0 @@
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
- ]