managed-deepagents 0.6.2.dev51__py3-none-win_amd64.whl → 0.6.2.dev52__py3-none-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- managed_deepagents/__init__.py +1 -3
- managed_deepagents/_agent_auth.py +127 -43
- managed_deepagents/_agent_auth_http.py +65 -9
- managed_deepagents/_binaries/mda.exe +0 -0
- managed_deepagents/_channels/trigger/http.py +141 -9
- managed_deepagents/_channels/trigger/identity_context.py +214 -0
- managed_deepagents/_channels/trigger/types.py +14 -2
- managed_deepagents/_connections.py +133 -49
- managed_deepagents/_connectors/__init__.py +3 -2
- managed_deepagents/_connectors/mcp/__init__.py +20 -51
- managed_deepagents/_connectors/mcp/load.py +5 -5
- managed_deepagents/_connectors/tool_selection.py +1 -1
- managed_deepagents/_credential_gate.py +5 -5
- managed_deepagents/_extension.py +3 -3
- managed_deepagents/_identity_auth.py +69 -8
- managed_deepagents/_identity_runtime.py +17 -0
- managed_deepagents/_ingress.py +48 -14
- managed_deepagents/_validated_token.py +35 -0
- managed_deepagents/identity/__init__.py +10 -0
- managed_deepagents/namespaces.py +14 -24
- {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/METADATA +22 -18
- {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/RECORD +26 -25
- {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/WHEEL +0 -0
- {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/entry_points.txt +0 -0
- {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/licenses/LICENSE +0 -0
- {managed_deepagents-0.6.2.dev51.dist-info → managed_deepagents-0.6.2.dev52.dist-info}/top_level.txt +0 -0
managed_deepagents/__init__.py
CHANGED
|
@@ -8,7 +8,7 @@ This package provides two things:
|
|
|
8
8
|
``mda`` console script.
|
|
9
9
|
|
|
10
10
|
Resources use ``define_*`` factories while provider collections use namespaces:
|
|
11
|
-
``define_sandbox()``, ``
|
|
11
|
+
``define_sandbox()``, ``channels.slack(...)``, ``connectors.mcp(...)``,
|
|
12
12
|
``connections.get(...)``, and ``auth.supabase(...)``. Contributing a *new*
|
|
13
13
|
primitive is not part of the public surface.
|
|
14
14
|
"""
|
|
@@ -32,7 +32,6 @@ from managed_deepagents.namespaces import (
|
|
|
32
32
|
auth,
|
|
33
33
|
channels,
|
|
34
34
|
connectors,
|
|
35
|
-
define_mcp,
|
|
36
35
|
define_sandbox,
|
|
37
36
|
)
|
|
38
37
|
from managed_deepagents.sandbox import SandboxDefinition
|
|
@@ -54,7 +53,6 @@ __all__ = [
|
|
|
54
53
|
"connectors",
|
|
55
54
|
"define_deep_agent",
|
|
56
55
|
"define_identity",
|
|
57
|
-
"define_mcp",
|
|
58
56
|
"define_memory",
|
|
59
57
|
"define_sandbox",
|
|
60
58
|
"define_schedule",
|
|
@@ -16,6 +16,7 @@ from managed_deepagents._agent_auth_http import (
|
|
|
16
16
|
CredentialStoreError,
|
|
17
17
|
agent_auth_credential_query,
|
|
18
18
|
agent_auth_headers,
|
|
19
|
+
missing_workspace_connection_error,
|
|
19
20
|
request_agent_auth_json,
|
|
20
21
|
)
|
|
21
22
|
|
|
@@ -23,6 +24,7 @@ AGENT_AUTH_PROVIDER_PAGE_SIZE = 100
|
|
|
23
24
|
AGENT_AUTH_MAX_PROVIDER_PAGES = 20
|
|
24
25
|
|
|
25
26
|
_HTTP_BAD_REQUEST = 400
|
|
27
|
+
_HTTP_FORBIDDEN = 403
|
|
26
28
|
_HTTP_NOT_FOUND = 404
|
|
27
29
|
_HTTP_CONFLICT = 409
|
|
28
30
|
|
|
@@ -33,21 +35,46 @@ __all__ = [
|
|
|
33
35
|
"AgentAuthUnknownProviderError",
|
|
34
36
|
"CredentialAuthorizationRequiredError",
|
|
35
37
|
"PendingAuthorization",
|
|
38
|
+
"ResolvePrincipalIdentity",
|
|
36
39
|
"WorkspaceConnection",
|
|
40
|
+
"clear_principal_resolve_cache_for_tests",
|
|
37
41
|
]
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
@dataclass(frozen=True)
|
|
41
45
|
class AgentAuthContext:
|
|
42
46
|
base_url: str
|
|
43
|
-
#: Deployment id, passed to oauth-authorization-sessions as ``agent_id``.
|
|
44
|
-
agent_id: str
|
|
45
47
|
#: Authenticated caller, used as ``owner_id`` for user credentials.
|
|
46
48
|
principal_id: str
|
|
49
|
+
#: Deployment id when present. Required for agent-owned lookups; optional for user OAuth.
|
|
50
|
+
agent_id: str | None = None
|
|
47
51
|
api_key: str | None = None
|
|
48
52
|
workspace_id: str | None = None
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ResolvePrincipalIdentity(TypedDict):
|
|
56
|
+
provider: str
|
|
57
|
+
issuer: str
|
|
58
|
+
subject: str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
_principal_resolve_cache: dict[str, str] = {}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def clear_principal_resolve_cache_for_tests() -> None:
|
|
65
|
+
"""Drop cached principal resolutions. Test-only."""
|
|
66
|
+
_principal_resolve_cache.clear()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _principal_resolve_cache_key(
|
|
70
|
+
context: AgentAuthContext, identity: ResolvePrincipalIdentity
|
|
71
|
+
) -> str:
|
|
72
|
+
origin = context.base_url.rstrip("/")
|
|
73
|
+
workspace = (context.workspace_id or "").strip()
|
|
74
|
+
return (
|
|
75
|
+
f"{origin}\0{workspace}\0"
|
|
76
|
+
f"{identity['provider']}\0{identity['issuer']}\0{identity['subject']}"
|
|
77
|
+
)
|
|
51
78
|
|
|
52
79
|
|
|
53
80
|
class PendingAuthorization(TypedDict):
|
|
@@ -116,6 +143,49 @@ class AgentAuth:
|
|
|
116
143
|
self._timeout = timeout
|
|
117
144
|
self._request: AgentAuthRequest = request if request is not None else self._urlopen_json
|
|
118
145
|
|
|
146
|
+
def resolve_principal(self, identity: ResolvePrincipalIdentity) -> str:
|
|
147
|
+
"""Resolve a verified external identity to an Agent Auth principal UUID."""
|
|
148
|
+
provider = identity["provider"].strip()
|
|
149
|
+
issuer = identity["issuer"].strip()
|
|
150
|
+
subject = identity["subject"].strip()
|
|
151
|
+
if not provider or not issuer or not subject:
|
|
152
|
+
raise CredentialStoreError(
|
|
153
|
+
"Agent Auth principal resolve requires provider, issuer, and subject"
|
|
154
|
+
)
|
|
155
|
+
key = _principal_resolve_cache_key(
|
|
156
|
+
self.context, {"provider": provider, "issuer": issuer, "subject": subject}
|
|
157
|
+
)
|
|
158
|
+
cached = _principal_resolve_cache.get(key)
|
|
159
|
+
if cached is not None:
|
|
160
|
+
return cached
|
|
161
|
+
principal_id = self._resolve_principal_uncached(
|
|
162
|
+
{"provider": provider, "issuer": issuer, "subject": subject}
|
|
163
|
+
)
|
|
164
|
+
_principal_resolve_cache[key] = principal_id
|
|
165
|
+
return principal_id
|
|
166
|
+
|
|
167
|
+
def _resolve_principal_uncached(self, identity: ResolvePrincipalIdentity) -> str:
|
|
168
|
+
status, body = self._request(
|
|
169
|
+
"POST",
|
|
170
|
+
"/v1/agent-auth/principals",
|
|
171
|
+
body={
|
|
172
|
+
"provider": identity["provider"],
|
|
173
|
+
"issuer": identity["issuer"],
|
|
174
|
+
"subject": identity["subject"],
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
if status >= _HTTP_BAD_REQUEST:
|
|
178
|
+
raise CredentialStoreError("Agent Auth resolve principal failed", status, body)
|
|
179
|
+
principal = body.get("principal")
|
|
180
|
+
principal_id = ""
|
|
181
|
+
if isinstance(principal, Mapping):
|
|
182
|
+
raw = principal.get("id")
|
|
183
|
+
if isinstance(raw, str):
|
|
184
|
+
principal_id = raw.strip()
|
|
185
|
+
if not principal_id:
|
|
186
|
+
raise CredentialStoreError("Agent Auth resolve principal returned no principal id")
|
|
187
|
+
return principal_id
|
|
188
|
+
|
|
119
189
|
def find_workspace_connection(self, slug: str) -> WorkspaceConnection | None:
|
|
120
190
|
"""List workspace connections and return the row for ``slug``, if any."""
|
|
121
191
|
if not (self.context.workspace_id or "").strip():
|
|
@@ -134,7 +204,7 @@ class AgentAuth:
|
|
|
134
204
|
f"/v1/agent-auth/connections?{urllib.parse.urlencode(query)}",
|
|
135
205
|
)
|
|
136
206
|
if status >= _HTTP_BAD_REQUEST:
|
|
137
|
-
raise CredentialStoreError("Agent Auth list connections failed", status)
|
|
207
|
+
raise CredentialStoreError("Agent Auth list connections failed", status, body)
|
|
138
208
|
raw_items = body.get("items")
|
|
139
209
|
items = raw_items if isinstance(raw_items, list) else []
|
|
140
210
|
for item in items:
|
|
@@ -170,31 +240,23 @@ class AgentAuth:
|
|
|
170
240
|
def find_secret_credential_id(self, connection: WorkspaceConnection, slug: str) -> str | None:
|
|
171
241
|
"""Resolve the credential id for a secret connection row.
|
|
172
242
|
|
|
173
|
-
|
|
174
|
-
|
|
243
|
+
Agent-owned connection declarations scope the credential to this
|
|
244
|
+
deployment. Workspace-owned credentials are deliberately not visible
|
|
245
|
+
to deployments.
|
|
175
246
|
"""
|
|
176
247
|
if connection.kind != "secret":
|
|
177
248
|
return None
|
|
178
|
-
|
|
179
|
-
if not
|
|
249
|
+
agent_id = (self.context.agent_id or "").strip()
|
|
250
|
+
if not agent_id:
|
|
180
251
|
raise CredentialStoreError(
|
|
181
|
-
"connections are not configured:
|
|
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,
|
|
252
|
+
"connections are not configured: LANGSMITH_HOST_PROJECT_ID is not set"
|
|
194
253
|
)
|
|
254
|
+
from_agent = self._credential_id_for_owner(connection.connection_id, "agent", agent_id)
|
|
255
|
+
if from_agent is not None:
|
|
256
|
+
return from_agent
|
|
195
257
|
return self.find_credential_id(
|
|
196
258
|
owner_type="agent",
|
|
197
|
-
owner_id=
|
|
259
|
+
owner_id=agent_id,
|
|
198
260
|
kind="secret",
|
|
199
261
|
slug=slug,
|
|
200
262
|
)
|
|
@@ -202,6 +264,12 @@ class AgentAuth:
|
|
|
202
264
|
def _credential_id_for_owner(
|
|
203
265
|
self, connection_id: str, owner_type: str, owner_id: str
|
|
204
266
|
) -> str | None:
|
|
267
|
+
"""Read credential_id from GET /connections/{id}?credential_owner_*.
|
|
268
|
+
|
|
269
|
+
Deployments authenticate with ``LANGSMITH_API_KEY`` (the minted
|
|
270
|
+
service-account key). User and workspace owners additionally require
|
|
271
|
+
workspace management permission.
|
|
272
|
+
"""
|
|
205
273
|
query = urllib.parse.urlencode(
|
|
206
274
|
{
|
|
207
275
|
"credential_owner_type": owner_type,
|
|
@@ -215,16 +283,27 @@ class AgentAuth:
|
|
|
215
283
|
if status == _HTTP_NOT_FOUND:
|
|
216
284
|
return None
|
|
217
285
|
if status >= _HTTP_BAD_REQUEST:
|
|
218
|
-
raise CredentialStoreError(
|
|
286
|
+
raise CredentialStoreError(
|
|
287
|
+
"Agent Auth get connection for owner failed"
|
|
288
|
+
+ (" (forbidden)" if status == _HTTP_FORBIDDEN else ""),
|
|
289
|
+
status,
|
|
290
|
+
body,
|
|
291
|
+
)
|
|
219
292
|
return _connection_credential_id(body)
|
|
220
293
|
|
|
221
294
|
def read_user_secret(self, slug: str) -> str | None:
|
|
222
295
|
"""Read a caller-owned opaque secret grant for a workspace connection slug."""
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
296
|
+
connection = self.find_workspace_connection(slug)
|
|
297
|
+
if connection is None:
|
|
298
|
+
return None
|
|
299
|
+
return self.read_user_secret_for_connection(connection)
|
|
300
|
+
|
|
301
|
+
def read_user_secret_for_connection(self, connection: WorkspaceConnection) -> str | None:
|
|
302
|
+
"""Read a caller-owned opaque secret when the connection row is already loaded."""
|
|
303
|
+
if connection.kind != "secret":
|
|
304
|
+
return None
|
|
305
|
+
credential_id = self._credential_id_for_owner(
|
|
306
|
+
connection.connection_id, "user", self.context.principal_id
|
|
228
307
|
)
|
|
229
308
|
if credential_id is None:
|
|
230
309
|
return None
|
|
@@ -244,11 +323,17 @@ class AgentAuth:
|
|
|
244
323
|
Returns:
|
|
245
324
|
The access token, or ``None`` when the caller must connect.
|
|
246
325
|
"""
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
326
|
+
connection = self.find_workspace_connection(slug)
|
|
327
|
+
if connection is None:
|
|
328
|
+
return None
|
|
329
|
+
return self.read_user_access_token_for_connection(connection)
|
|
330
|
+
|
|
331
|
+
def read_user_access_token_for_connection(self, connection: WorkspaceConnection) -> str | None:
|
|
332
|
+
"""Read a caller OAuth token when the connection row is already loaded."""
|
|
333
|
+
if connection.kind != "oauth2":
|
|
334
|
+
return None
|
|
335
|
+
credential_id = self._credential_id_for_owner(
|
|
336
|
+
connection.connection_id, "user", self.context.principal_id
|
|
252
337
|
)
|
|
253
338
|
if credential_id is None:
|
|
254
339
|
return None
|
|
@@ -258,7 +343,7 @@ class AgentAuth:
|
|
|
258
343
|
"""Start an OAuth session for a slug the caller has not granted."""
|
|
259
344
|
connection = self.find_workspace_connection(slug)
|
|
260
345
|
if connection is None:
|
|
261
|
-
raise
|
|
346
|
+
raise missing_workspace_connection_error(slug)
|
|
262
347
|
if connection.kind != "oauth2":
|
|
263
348
|
raise CredentialStoreError(f"connection {slug!r} is not an OAuth connection")
|
|
264
349
|
status, body = self._request(
|
|
@@ -266,13 +351,13 @@ class AgentAuth:
|
|
|
266
351
|
"/v1/agent-auth/oauth-authorization-sessions",
|
|
267
352
|
body={
|
|
268
353
|
"connection_id": connection.connection_id,
|
|
269
|
-
"
|
|
270
|
-
"
|
|
354
|
+
"owner_type": "user",
|
|
355
|
+
"owner_id": self.context.principal_id,
|
|
271
356
|
},
|
|
272
357
|
)
|
|
273
358
|
if status >= _HTTP_BAD_REQUEST:
|
|
274
359
|
raise CredentialStoreError(
|
|
275
|
-
"Agent Auth create oauth-authorization-session failed", status
|
|
360
|
+
"Agent Auth create oauth-authorization-session failed", status, body
|
|
276
361
|
)
|
|
277
362
|
auth_id = str(body.get("id") or "")
|
|
278
363
|
connect_url = str(body.get("verification_url") or "")
|
|
@@ -293,7 +378,7 @@ class AgentAuth:
|
|
|
293
378
|
"GET", f"/v1/agent-auth/oauth-providers?{urllib.parse.urlencode(query)}"
|
|
294
379
|
)
|
|
295
380
|
if status >= _HTTP_BAD_REQUEST:
|
|
296
|
-
raise CredentialStoreError("Agent Auth list oauth-providers failed", status)
|
|
381
|
+
raise CredentialStoreError("Agent Auth list oauth-providers failed", status, body)
|
|
297
382
|
raw_items = body.get("items")
|
|
298
383
|
for item in raw_items if isinstance(raw_items, list) else []:
|
|
299
384
|
if isinstance(item, Mapping) and item.get("slug") == slug and item.get("id"):
|
|
@@ -329,7 +414,7 @@ class AgentAuth:
|
|
|
329
414
|
)
|
|
330
415
|
status, body = self._request("GET", f"/v1/agent-auth/credentials?{query}")
|
|
331
416
|
if status >= _HTTP_BAD_REQUEST:
|
|
332
|
-
raise CredentialStoreError("Agent Auth list credentials failed", status)
|
|
417
|
+
raise CredentialStoreError("Agent Auth list credentials failed", status, body)
|
|
333
418
|
raw_items = body.get("items")
|
|
334
419
|
items = raw_items if isinstance(raw_items, list) else []
|
|
335
420
|
for item in items:
|
|
@@ -352,7 +437,7 @@ class AgentAuth:
|
|
|
352
437
|
if status == _HTTP_NOT_FOUND:
|
|
353
438
|
return None
|
|
354
439
|
if status >= _HTTP_BAD_REQUEST:
|
|
355
|
-
raise CredentialStoreError("Agent Auth get secret failed", status)
|
|
440
|
+
raise CredentialStoreError("Agent Auth get secret failed", status, body)
|
|
356
441
|
secret = body.get("secret")
|
|
357
442
|
return secret if isinstance(secret, str) and secret else None
|
|
358
443
|
|
|
@@ -364,7 +449,7 @@ class AgentAuth:
|
|
|
364
449
|
if _is_authorization_required(status, body):
|
|
365
450
|
return None
|
|
366
451
|
if status >= _HTTP_BAD_REQUEST:
|
|
367
|
-
raise CredentialStoreError("Agent Auth get oauth2 failed", status)
|
|
452
|
+
raise CredentialStoreError("Agent Auth get oauth2 failed", status, body)
|
|
368
453
|
oauth2 = body.get("oauth2") if isinstance(body.get("oauth2"), dict) else body
|
|
369
454
|
token = oauth2.get("access_token") if isinstance(oauth2, dict) else None
|
|
370
455
|
return token if isinstance(token, str) and token else None
|
|
@@ -390,7 +475,6 @@ class AgentAuth:
|
|
|
390
475
|
return agent_auth_headers(
|
|
391
476
|
api_key=self.context.api_key,
|
|
392
477
|
workspace_id=self.context.workspace_id,
|
|
393
|
-
service_key=self.context.service_key,
|
|
394
478
|
)
|
|
395
479
|
|
|
396
480
|
|
|
@@ -7,36 +7,96 @@ API with the same headers, redirect policy, and page walker.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
|
+
import re
|
|
10
11
|
import urllib.error
|
|
11
12
|
import urllib.parse
|
|
12
13
|
import urllib.request
|
|
13
14
|
from collections.abc import Mapping
|
|
14
|
-
from typing import TYPE_CHECKING
|
|
15
|
+
from typing import TYPE_CHECKING, Final
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
from http.client import HTTPMessage
|
|
18
19
|
from typing import IO
|
|
19
20
|
|
|
20
21
|
AGENT_AUTH_TIMEOUT_SECONDS = 10.0
|
|
22
|
+
AGENT_AUTH_PROBLEM_HINT_MAX_CHARS = 200
|
|
23
|
+
_PROBLEM_HINT_FIELDS: Final = ("detail", "remedy")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def agent_auth_problem_hint(body: Mapping[str, object] | None) -> str | None:
|
|
27
|
+
"""Return the caller-facing part of an Agent Auth problem+json body.
|
|
28
|
+
|
|
29
|
+
``detail`` names the failure and ``remedy`` names the fix. Nothing else
|
|
30
|
+
from a response body is read, because the rest can quote secret material.
|
|
31
|
+
Both are collapsed to one line and truncated so a long or multi-line body
|
|
32
|
+
cannot reshape an error message.
|
|
33
|
+
"""
|
|
34
|
+
seen: dict[str, None] = {}
|
|
35
|
+
for field in _PROBLEM_HINT_FIELDS:
|
|
36
|
+
value = None if body is None else body.get(field)
|
|
37
|
+
if not isinstance(value, str):
|
|
38
|
+
continue
|
|
39
|
+
text = " ".join(value.split())
|
|
40
|
+
if text:
|
|
41
|
+
seen[text] = None
|
|
42
|
+
if not seen:
|
|
43
|
+
return None
|
|
44
|
+
hint = " ".join(seen)
|
|
45
|
+
if len(hint) > AGENT_AUTH_PROBLEM_HINT_MAX_CHARS:
|
|
46
|
+
return hint[:AGENT_AUTH_PROBLEM_HINT_MAX_CHARS] + "..."
|
|
47
|
+
return hint
|
|
21
48
|
|
|
22
49
|
|
|
23
50
|
class CredentialStoreError(RuntimeError):
|
|
24
51
|
"""Agent Auth could not answer.
|
|
25
52
|
|
|
26
|
-
Callers pass a fixed message
|
|
27
|
-
|
|
53
|
+
Callers pass a fixed message; the only part of a response body that reaches
|
|
54
|
+
this error, the logs, or the traceback chain is the allow-listed
|
|
55
|
+
problem+json hint, since an arbitrary body can quote secret material.
|
|
28
56
|
"""
|
|
29
57
|
|
|
30
|
-
def __init__(
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
message: str,
|
|
61
|
+
status: int | None = None,
|
|
62
|
+
problem: Mapping[str, object] | None = None,
|
|
63
|
+
) -> None:
|
|
31
64
|
self.status = status
|
|
32
|
-
|
|
65
|
+
with_status = message if status is None else f"{message} (status {status})"
|
|
66
|
+
hint = agent_auth_problem_hint(problem)
|
|
67
|
+
super().__init__(with_status if hint is None else f"{with_status}: {hint}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Each runtime quotes the slug its own way: repr() here, JSON.stringify in npm.
|
|
71
|
+
_MISSING_WORKSPACE_CONNECTION = re.compile(
|
|
72
|
+
r"""workspace connection ['"]([A-Za-z0-9][A-Za-z0-9._-]*)['"] is not configured"""
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def missing_workspace_connection_error(slug: str) -> CredentialStoreError:
|
|
77
|
+
"""Return the error for a declared slug that has no workspace connection row."""
|
|
78
|
+
return CredentialStoreError(f"workspace connection {slug!r} is not configured")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def missing_workspace_connection_slug(text: str) -> str | None:
|
|
82
|
+
"""Read the slug back out of a missing-connection message.
|
|
83
|
+
|
|
84
|
+
A run that fails in the gate reaches a channel as a plain string, so this is
|
|
85
|
+
the only way back to the slug. It must move with the message it parses.
|
|
86
|
+
"""
|
|
87
|
+
match = _MISSING_WORKSPACE_CONNECTION.search(text)
|
|
88
|
+
return None if match is None else match.group(1)
|
|
33
89
|
|
|
34
90
|
|
|
35
91
|
__all__ = [
|
|
92
|
+
"AGENT_AUTH_PROBLEM_HINT_MAX_CHARS",
|
|
36
93
|
"AGENT_AUTH_TIMEOUT_SECONDS",
|
|
37
94
|
"CredentialStoreError",
|
|
38
95
|
"agent_auth_credential_query",
|
|
39
96
|
"agent_auth_headers",
|
|
97
|
+
"agent_auth_problem_hint",
|
|
98
|
+
"missing_workspace_connection_error",
|
|
99
|
+
"missing_workspace_connection_slug",
|
|
40
100
|
"request_agent_auth_json",
|
|
41
101
|
]
|
|
42
102
|
|
|
@@ -70,16 +130,12 @@ def agent_auth_headers(
|
|
|
70
130
|
*,
|
|
71
131
|
api_key: str | None = None,
|
|
72
132
|
workspace_id: str | None = None,
|
|
73
|
-
service_key: str | None = None,
|
|
74
133
|
) -> dict[str, str]:
|
|
75
134
|
headers = {
|
|
76
135
|
"accept": "application/json",
|
|
77
136
|
"content-type": "application/json",
|
|
78
137
|
"x-langsmith-product": "mda",
|
|
79
138
|
}
|
|
80
|
-
resolved_service_key = (service_key or "").strip()
|
|
81
|
-
if resolved_service_key:
|
|
82
|
-
headers["x-service-key"] = resolved_service_key
|
|
83
139
|
resolved_api_key = (api_key or "").strip()
|
|
84
140
|
if resolved_api_key:
|
|
85
141
|
headers["x-api-key"] = resolved_api_key
|
|
Binary file
|