managed-deepagents 0.6.2.dev30__py3-none-win_amd64.whl → 0.6.2.dev32__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.
@@ -9,18 +9,18 @@ This package provides two things:
9
9
 
10
10
  Resources use ``define_*`` factories while provider collections use namespaces:
11
11
  ``define_sandbox()``, ``channels.slack(...)``, ``connectors.mcp(...)``,
12
- ``credentials.get(...)``, and ``auth.supabase(...)``. Contributing a *new*
12
+ ``connections.get(...)``, and ``auth.supabase(...)``. Contributing a *new*
13
13
  primitive is not part of the public surface.
14
14
  """
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
- from managed_deepagents._credentials import (
18
+ from managed_deepagents._connections import (
19
+ ConnectionNotFoundError,
19
20
  CredentialAuthorizationRequiredError,
20
- CredentialNotFoundError,
21
- CredentialOwner,
22
21
  CredentialStoreError,
23
- credentials,
22
+ InvalidAgentAuthEndpointError,
23
+ connections,
24
24
  )
25
25
  from managed_deepagents._managed_tools import (
26
26
  ManagedDeepAgentRuntime,
@@ -38,19 +38,19 @@ from managed_deepagents.sandbox import SandboxDefinition
38
38
  from managed_deepagents.schedules import define_schedule
39
39
 
40
40
  __all__ = [
41
+ "ConnectionNotFoundError",
41
42
  "CredentialAuthorizationRequiredError",
42
- "CredentialNotFoundError",
43
- "CredentialOwner",
44
43
  "CredentialStoreError",
45
44
  "DeepAgentDefinition",
45
+ "InvalidAgentAuthEndpointError",
46
46
  "ManagedDeepAgentRuntime",
47
47
  "ManagedServerInfo",
48
48
  "MemoryDefinition",
49
49
  "SandboxDefinition",
50
50
  "auth",
51
51
  "channels",
52
+ "connections",
52
53
  "connectors",
53
- "credentials",
54
54
  "define_deep_agent",
55
55
  "define_identity",
56
56
  "define_memory",
@@ -1,4 +1,4 @@
1
- """Agent Auth client used by ``credentials.get`` and the pre-run credential gate.
1
+ """Agent Auth client used by ``connections.get`` and the pre-run credential gate.
2
2
 
3
3
  Talks to the shipped Agent Auth APIs (credentials + oauth-authorization-sessions),
4
4
  not the aspirational ``credentials/mint`` endpoint.
@@ -33,6 +33,7 @@ __all__ = [
33
33
  "AgentAuthUnknownProviderError",
34
34
  "CredentialAuthorizationRequiredError",
35
35
  "PendingAuthorization",
36
+ "WorkspaceConnection",
36
37
  ]
37
38
 
38
39
 
@@ -45,20 +46,29 @@ class AgentAuthContext:
45
46
  principal_id: str
46
47
  api_key: str | None = None
47
48
  workspace_id: str | None = None
49
+ #: Agent Builder service JWT for workspace-owned credential material.
50
+ service_key: str | None = None
48
51
 
49
52
 
50
53
  class PendingAuthorization(TypedDict):
51
- """One credential slug the caller has not granted, with its connect URL."""
54
+ """One connection slug the caller has not granted, with its connect URL."""
52
55
 
53
56
  slug: str
54
57
  connect_url: str
55
58
  auth_id: str
56
59
 
57
60
 
61
+ @dataclass(frozen=True)
62
+ class WorkspaceConnection:
63
+ connection_id: str
64
+ kind: str
65
+ owner_policy: str
66
+
67
+
58
68
  class CredentialAuthorizationRequiredError(RuntimeError):
59
69
  def __init__(self, slugs: list[str]) -> None:
60
70
  super().__init__(
61
- f"the caller has not connected: {', '.join(slugs)}. Declare the credential "
71
+ f"the caller has not connected: {', '.join(slugs)}. Declare the connection "
62
72
  "on a connector so the pre-run gate can request it."
63
73
  )
64
74
  self.slugs = slugs
@@ -106,6 +116,124 @@ class AgentAuth:
106
116
  self._timeout = timeout
107
117
  self._request: AgentAuthRequest = request if request is not None else self._urlopen_json
108
118
 
119
+ def find_workspace_connection(self, slug: str) -> WorkspaceConnection | None:
120
+ """List workspace connections and return the row for ``slug``, if any."""
121
+ if not (self.context.workspace_id or "").strip():
122
+ raise CredentialStoreError(
123
+ "connections are not configured: LANGSMITH_WORKSPACE_ID is not set"
124
+ )
125
+ cursor: str | None = None
126
+ for _ in range(AGENT_AUTH_MAX_PROVIDER_PAGES):
127
+ query: dict[str, str] = {
128
+ "page_size": str(AGENT_AUTH_PROVIDER_PAGE_SIZE),
129
+ }
130
+ if cursor:
131
+ query["cursor"] = cursor
132
+ status, body = self._request(
133
+ "GET",
134
+ f"/v1/agent-auth/connections?{urllib.parse.urlencode(query)}",
135
+ )
136
+ if status >= _HTTP_BAD_REQUEST:
137
+ raise CredentialStoreError("Agent Auth list connections failed", status)
138
+ raw_items = body.get("items")
139
+ items = raw_items if isinstance(raw_items, list) else []
140
+ for item in items:
141
+ if not isinstance(item, Mapping) or item.get("slug") != slug:
142
+ continue
143
+ connection_id = (
144
+ item.get("id") or item.get("connection_id") or item.get("credential_id")
145
+ )
146
+ kind = _connection_credential_kind(item)
147
+ owner_policy = item.get("owner_policy")
148
+ if not isinstance(connection_id, str) or not connection_id:
149
+ raise CredentialStoreError("Agent Auth connection row missing id or kind")
150
+ if not isinstance(kind, str) or not kind:
151
+ raise CredentialStoreError("Agent Auth connection row missing id or kind")
152
+ return WorkspaceConnection(
153
+ connection_id=connection_id,
154
+ kind=kind,
155
+ owner_policy=str(owner_policy) if isinstance(owner_policy, str) else "",
156
+ )
157
+ next_cursor = body.get("next_cursor")
158
+ if not isinstance(next_cursor, str) or not next_cursor or next_cursor == cursor:
159
+ return None
160
+ cursor = next_cursor
161
+ raise CredentialStoreError("Agent Auth connection list returned too many pages")
162
+
163
+ def find_workspace_credential_id(self, slug: str) -> str | None:
164
+ """Workspace credential id for an agent-owned opaque secret grant."""
165
+ connection = self.find_workspace_connection(slug)
166
+ if connection is None:
167
+ return None
168
+ return self.find_secret_credential_id(connection, slug)
169
+
170
+ def find_secret_credential_id(self, connection: WorkspaceConnection, slug: str) -> str | None:
171
+ """Resolve the credential id for a secret connection row.
172
+
173
+ With an Agent Builder service key, prefer GET-for-owner / workspace list.
174
+ Personal API keys may only list agent-owned credentials.
175
+ """
176
+ if connection.kind != "secret":
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
+ )
195
+ return self.find_credential_id(
196
+ owner_type="agent",
197
+ owner_id=self.context.agent_id,
198
+ kind="secret",
199
+ slug=slug,
200
+ )
201
+
202
+ def _credential_id_for_owner(
203
+ self, connection_id: str, owner_type: str, owner_id: str
204
+ ) -> str | None:
205
+ query = urllib.parse.urlencode(
206
+ {
207
+ "credential_owner_type": owner_type,
208
+ "credential_owner_id": owner_id,
209
+ }
210
+ )
211
+ status, body = self._request(
212
+ "GET",
213
+ f"/v1/agent-auth/connections/{_path_segment(connection_id)}?{query}",
214
+ )
215
+ if status == _HTTP_NOT_FOUND:
216
+ return None
217
+ if status >= _HTTP_BAD_REQUEST:
218
+ raise CredentialStoreError("Agent Auth get connection for owner failed", status)
219
+ return _connection_credential_id(body)
220
+
221
+ def read_user_secret(self, slug: str) -> str | None:
222
+ """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,
228
+ )
229
+ if credential_id is None:
230
+ return None
231
+ return self.read_secret(credential_id)
232
+
233
+ def read_connection_access_token(self, connection_id: str) -> str | None:
234
+ """Read an OAuth access token stored on a workspace connection row."""
235
+ return self._fetch_access_token(connection_id)
236
+
109
237
  def read_user_access_token(self, slug: str) -> str | None:
110
238
  """Read the caller's OAuth access token without starting a session.
111
239
 
@@ -128,12 +256,16 @@ class AgentAuth:
128
256
 
129
257
  def start_authorization_session(self, slug: str) -> PendingAuthorization:
130
258
  """Start an OAuth session for a slug the caller has not granted."""
131
- provider_id = self.resolve_oauth_provider_id(slug)
259
+ connection = self.find_workspace_connection(slug)
260
+ if connection is None:
261
+ raise CredentialStoreError(f"workspace connection {slug!r} is not configured")
262
+ if connection.kind != "oauth2":
263
+ raise CredentialStoreError(f"connection {slug!r} is not an OAuth connection")
132
264
  status, body = self._request(
133
265
  "POST",
134
266
  "/v1/agent-auth/oauth-authorization-sessions",
135
267
  body={
136
- "provider_id": provider_id,
268
+ "connection_id": connection.connection_id,
137
269
  "principal_id": self.context.principal_id,
138
270
  "agent_id": self.context.agent_id,
139
271
  },
@@ -182,18 +314,35 @@ class AgentAuth:
182
314
  ) -> str | None:
183
315
  """Find a credential id by slug.
184
316
 
185
- The credential key is ``credential_id``, never ``id``.
317
+ The shipped credentials list filters by owner and kind only, so the
318
+ slug match is made client-side across pages. The credential key is
319
+ ``credential_id``, never ``id``.
186
320
  """
187
- query = agent_auth_credential_query(
188
- owner_type=owner_type, owner_id=owner_id, kind=kind, slug=slug
189
- )
190
- status, body = self._request("GET", f"/v1/agent-auth/credentials?{query}")
191
- if status >= _HTTP_BAD_REQUEST:
192
- raise CredentialStoreError("Agent Auth list credentials failed", status)
193
- items = body.get("items")
194
- row = items[0] if isinstance(items, list) and items else None
195
- credential_id = row.get("credential_id") if isinstance(row, Mapping) else None
196
- return credential_id if isinstance(credential_id, str) else None
321
+ cursor: str | None = None
322
+ for _ in range(AGENT_AUTH_MAX_PROVIDER_PAGES):
323
+ query = agent_auth_credential_query(
324
+ owner_type=owner_type,
325
+ owner_id=owner_id,
326
+ kind=kind,
327
+ page_size=AGENT_AUTH_PROVIDER_PAGE_SIZE,
328
+ cursor=cursor,
329
+ )
330
+ status, body = self._request("GET", f"/v1/agent-auth/credentials?{query}")
331
+ if status >= _HTTP_BAD_REQUEST:
332
+ raise CredentialStoreError("Agent Auth list credentials failed", status)
333
+ raw_items = body.get("items")
334
+ items = raw_items if isinstance(raw_items, list) else []
335
+ for item in items:
336
+ if not isinstance(item, Mapping) or item.get("slug") != slug:
337
+ continue
338
+ credential_id = item.get("credential_id")
339
+ if isinstance(credential_id, str) and credential_id:
340
+ return credential_id
341
+ next_cursor = body.get("next_cursor")
342
+ if not isinstance(next_cursor, str) or not next_cursor or next_cursor == cursor:
343
+ return None
344
+ cursor = next_cursor
345
+ raise CredentialStoreError("Agent Auth credential list returned too many pages")
197
346
 
198
347
  def read_secret(self, credential_id: str) -> str | None:
199
348
  status, body = self._request(
@@ -241,6 +390,7 @@ class AgentAuth:
241
390
  return agent_auth_headers(
242
391
  api_key=self.context.api_key,
243
392
  workspace_id=self.context.workspace_id,
393
+ service_key=self.context.service_key,
244
394
  )
245
395
 
246
396
 
@@ -248,6 +398,24 @@ def _path_segment(value: str) -> str:
248
398
  return urllib.parse.quote(value, safe="")
249
399
 
250
400
 
401
+ def _connection_credential_kind(item: Mapping[str, object]) -> str:
402
+ credential = item.get("credential")
403
+ if isinstance(credential, Mapping):
404
+ kind = credential.get("kind")
405
+ if isinstance(kind, str) and kind:
406
+ return kind
407
+ kind = item.get("kind")
408
+ return kind if isinstance(kind, str) else ""
409
+
410
+
411
+ def _connection_credential_id(body: Mapping[str, object]) -> str | None:
412
+ credential = body.get("credential")
413
+ if not isinstance(credential, Mapping):
414
+ return None
415
+ credential_id = credential.get("credential_id")
416
+ return credential_id if isinstance(credential_id, str) and credential_id else None
417
+
418
+
251
419
  def _is_authorization_required(status: int, body: dict[str, object]) -> bool:
252
420
  """Agent Auth answers 409 when a stored grant has lapsed and needs re-auth."""
253
421
  if status != _HTTP_CONFLICT:
@@ -46,24 +46,40 @@ def agent_auth_credential_query(
46
46
  owner_type: str,
47
47
  owner_id: str,
48
48
  kind: str,
49
- slug: str,
49
+ page_size: int | None = None,
50
+ cursor: str | None = None,
50
51
  ) -> str:
51
- """Query string that addresses one credential by its natural key."""
52
- return urllib.parse.urlencode(
53
- {"owner_type": owner_type, "owner_id": owner_id, "kind": kind, "slug": slug}
54
- )
52
+ """Query string for listing credentials filtered by owner and kind.
53
+
54
+ The shipped list endpoint does not filter by slug; callers match ``slug``
55
+ client-side across pages.
56
+ """
57
+ params: dict[str, str] = {
58
+ "owner_type": owner_type,
59
+ "owner_id": owner_id,
60
+ "kind": kind,
61
+ }
62
+ if page_size is not None:
63
+ params["page_size"] = str(page_size)
64
+ if cursor:
65
+ params["cursor"] = cursor
66
+ return urllib.parse.urlencode(params)
55
67
 
56
68
 
57
69
  def agent_auth_headers(
58
70
  *,
59
71
  api_key: str | None = None,
60
72
  workspace_id: str | None = None,
73
+ service_key: str | None = None,
61
74
  ) -> dict[str, str]:
62
75
  headers = {
63
76
  "accept": "application/json",
64
77
  "content-type": "application/json",
65
78
  "x-langsmith-product": "mda",
66
79
  }
80
+ resolved_service_key = (service_key or "").strip()
81
+ if resolved_service_key:
82
+ headers["x-service-key"] = resolved_service_key
67
83
  resolved_api_key = (api_key or "").strip()
68
84
  if resolved_api_key:
69
85
  headers["x-api-key"] = resolved_api_key
Binary file
@@ -17,6 +17,8 @@ JsonObject = dict[str, JsonValue]
17
17
  class _OptionalChannelManifest(TypedDict, total=False):
18
18
  transport: Literal["trigger_server"]
19
19
  userCredentials: list[str]
20
+ userConnections: list[str]
21
+ agentConnections: list[str]
20
22
 
21
23
 
22
24
  class ChannelManifest(_OptionalChannelManifest):
@@ -250,7 +250,7 @@ async def _observe_accepted_run(
250
250
  event["channel_id"],
251
251
  {
252
252
  "type": "post",
253
- "action_id": str(uuid.uuid4()),
253
+ "action_id": event["event_id"],
254
254
  "address": event["address"],
255
255
  "source_thread_key": event["source_thread_key"],
256
256
  "message": {"text": text},