agentonboard-sdk 0.1.0__py3-none-any.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.
@@ -0,0 +1,60 @@
1
+ """
2
+ AgentOnboard Python SDK — Universal identity layer for AI agents.
3
+ """
4
+
5
+ from agentonboard.account import (
6
+ AccountCreator,
7
+ AccountOutcome,
8
+ AccountResolver,
9
+ AgentIdentity,
10
+ AsyncAccountCreator,
11
+ AsyncAccountResolver,
12
+ create_agent_account,
13
+ create_agent_account_async,
14
+ normalize_email,
15
+ verify_agent_account,
16
+ verify_agent_account_async,
17
+ )
18
+ from agentonboard.codes import (
19
+ ACCOUNT_ERROR_DESCRIPTIONS,
20
+ VERIFY_ERROR_DESCRIPTIONS,
21
+ AccountErrorCode,
22
+ VerifyErrorCode,
23
+ status_from_code,
24
+ )
25
+ from agentonboard.verify import (
26
+ DEFAULT_CLOCK_TOLERANCE_SECONDS,
27
+ Verifier,
28
+ VerifyAgentTokenResult,
29
+ normalize_audience,
30
+ verify_agent_token,
31
+ verify_agent_token_async,
32
+ )
33
+
34
+ __version__ = "0.1.0"
35
+
36
+ __all__ = [
37
+ "ACCOUNT_ERROR_DESCRIPTIONS",
38
+ "AccountCreator",
39
+ "AccountErrorCode",
40
+ "AccountOutcome",
41
+ "AccountResolver",
42
+ "AgentIdentity",
43
+ "AsyncAccountCreator",
44
+ "AsyncAccountResolver",
45
+ "DEFAULT_CLOCK_TOLERANCE_SECONDS",
46
+ "VERIFY_ERROR_DESCRIPTIONS",
47
+ "Verifier",
48
+ "VerifyAgentTokenResult",
49
+ "VerifyErrorCode",
50
+ "__version__",
51
+ "create_agent_account",
52
+ "create_agent_account_async",
53
+ "normalize_audience",
54
+ "normalize_email",
55
+ "status_from_code",
56
+ "verify_agent_account",
57
+ "verify_agent_account_async",
58
+ "verify_agent_token",
59
+ "verify_agent_token_async",
60
+ ]
@@ -0,0 +1,320 @@
1
+ """
2
+ Account mapping and automated provisioning boundaries for AgentOnboard.
3
+
4
+ Implements verify_agent_account and create_agent_account per ADR 0002 and
5
+ docs/decisions/0007-agents-can-create-partner-accounts.md.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import inspect
11
+ from collections.abc import Awaitable, Callable
12
+ from dataclasses import dataclass
13
+ from typing import Any, Generic, TypeVar
14
+
15
+ from agentonboard.codes import AccountErrorCode
16
+ from agentonboard.verify import (
17
+ DEFAULT_CLOCK_TOLERANCE_SECONDS,
18
+ verify_agent_token,
19
+ verify_agent_token_async,
20
+ )
21
+
22
+ TUser = TypeVar("TUser")
23
+
24
+
25
+ @dataclass(slots=True, frozen=True)
26
+ class AgentIdentity:
27
+ """The verified identity, and the only thing that crosses into your resolver/creator."""
28
+
29
+ email: str
30
+
31
+
32
+ def normalize_email(email: str) -> str:
33
+ """Normalizes an email address for database lookups by trimming whitespace and converting to lowercase."""
34
+ if not email or not isinstance(email, str):
35
+ return ""
36
+ return email.strip().lower()
37
+
38
+
39
+ @dataclass(slots=True, frozen=True)
40
+ class AccountOutcome(Generic[TUser]):
41
+ ok: bool
42
+ email: str | None = None
43
+ user: TUser | None = None
44
+ created: bool = False
45
+ code: AccountErrorCode | None = None
46
+ error: str | None = None
47
+
48
+
49
+ AccountResolver = Callable[[AgentIdentity], TUser | None]
50
+ AccountCreator = Callable[[AgentIdentity], TUser]
51
+ AsyncAccountResolver = Callable[[AgentIdentity], Awaitable[TUser | None] | TUser | None]
52
+ AsyncAccountCreator = Callable[[AgentIdentity], Awaitable[TUser] | TUser]
53
+
54
+
55
+ def verify_agent_account(
56
+ token: str,
57
+ *,
58
+ audience: str,
59
+ resolver: AccountResolver[TUser],
60
+ issuer: str | None = None,
61
+ jwks_url: str | None = None,
62
+ jwks: dict[str, Any] | None = None,
63
+ fetch_jwks: Callable[[str], dict[str, Any]] | None = None,
64
+ clock_tolerance_seconds: int = DEFAULT_CLOCK_TOLERANCE_SECONDS,
65
+ ) -> AccountOutcome[TUser]:
66
+ """
67
+ Verifies an agent session token, then maps the verified email to your own user.
68
+ Never creates an account. If no account exists, returns ACCOUNT_REQUIRED.
69
+ """
70
+ verify_result = verify_agent_token(
71
+ token=token,
72
+ audience=audience,
73
+ issuer=issuer,
74
+ jwks_url=jwks_url,
75
+ jwks=jwks,
76
+ fetch_jwks=fetch_jwks,
77
+ clock_tolerance_seconds=clock_tolerance_seconds,
78
+ )
79
+
80
+ if not verify_result.ok:
81
+ return AccountOutcome(
82
+ ok=False,
83
+ code=verify_result.code,
84
+ error=verify_result.error,
85
+ )
86
+
87
+ verified_email = verify_result.email or ""
88
+ identity = AgentIdentity(email=verified_email)
89
+
90
+ try:
91
+ user = resolver(identity)
92
+ except Exception:
93
+ return AccountOutcome(
94
+ ok=False,
95
+ code="RESOLVER_ERROR",
96
+ error="The account resolver threw. This is a fault in the resolver, not the token.",
97
+ )
98
+
99
+ if user is None:
100
+ return AccountOutcome(
101
+ ok=False,
102
+ code="ACCOUNT_REQUIRED",
103
+ error="This identity has no account at your service; send the human to connect their account, then retry.",
104
+ )
105
+
106
+ return AccountOutcome(
107
+ ok=True,
108
+ email=verified_email,
109
+ user=user,
110
+ created=False,
111
+ )
112
+
113
+
114
+ async def verify_agent_account_async(
115
+ token: str,
116
+ *,
117
+ audience: str,
118
+ resolver: AsyncAccountResolver[TUser],
119
+ issuer: str | None = None,
120
+ jwks_url: str | None = None,
121
+ jwks: dict[str, Any] | None = None,
122
+ fetch_jwks: Callable[[str], dict[str, Any]] | None = None,
123
+ fetch_jwks_async: Callable[[str], Awaitable[dict[str, Any]]] | None = None,
124
+ clock_tolerance_seconds: int = DEFAULT_CLOCK_TOLERANCE_SECONDS,
125
+ ) -> AccountOutcome[TUser]:
126
+ """Asynchronous variant of verify_agent_account."""
127
+ verify_result = await verify_agent_token_async(
128
+ token=token,
129
+ audience=audience,
130
+ issuer=issuer,
131
+ jwks_url=jwks_url,
132
+ jwks=jwks,
133
+ fetch_jwks=fetch_jwks,
134
+ fetch_jwks_async=fetch_jwks_async,
135
+ clock_tolerance_seconds=clock_tolerance_seconds,
136
+ )
137
+
138
+ if not verify_result.ok:
139
+ return AccountOutcome(
140
+ ok=False,
141
+ code=verify_result.code,
142
+ error=verify_result.error,
143
+ )
144
+
145
+ verified_email = verify_result.email or ""
146
+ identity = AgentIdentity(email=verified_email)
147
+
148
+ try:
149
+ user_val = resolver(identity)
150
+ if inspect.isawaitable(user_val):
151
+ user = await user_val
152
+ else:
153
+ user = user_val
154
+ except Exception:
155
+ return AccountOutcome(
156
+ ok=False,
157
+ code="RESOLVER_ERROR",
158
+ error="The account resolver threw. This is a fault in the resolver, not the token.",
159
+ )
160
+
161
+ if user is None:
162
+ return AccountOutcome(
163
+ ok=False,
164
+ code="ACCOUNT_REQUIRED",
165
+ error="This identity has no account at your service; send the human to connect their account, then retry.",
166
+ )
167
+
168
+ return AccountOutcome(
169
+ ok=True,
170
+ email=verified_email,
171
+ user=user,
172
+ created=False,
173
+ )
174
+
175
+
176
+ def create_agent_account(
177
+ token: str,
178
+ *,
179
+ audience: str,
180
+ resolver: AccountResolver[TUser],
181
+ creator: AccountCreator[TUser],
182
+ issuer: str | None = None,
183
+ jwks_url: str | None = None,
184
+ jwks: dict[str, Any] | None = None,
185
+ fetch_jwks: Callable[[str], dict[str, Any]] | None = None,
186
+ clock_tolerance_seconds: int = DEFAULT_CLOCK_TOLERANCE_SECONDS,
187
+ ) -> AccountOutcome[TUser]:
188
+ """
189
+ Verifies an agent session token, maps the email to a user, and if no user exists,
190
+ calls creator once to provision the account.
191
+ """
192
+ verify_result = verify_agent_token(
193
+ token=token,
194
+ audience=audience,
195
+ issuer=issuer,
196
+ jwks_url=jwks_url,
197
+ jwks=jwks,
198
+ fetch_jwks=fetch_jwks,
199
+ clock_tolerance_seconds=clock_tolerance_seconds,
200
+ )
201
+
202
+ if not verify_result.ok:
203
+ return AccountOutcome(
204
+ ok=False,
205
+ code=verify_result.code,
206
+ error=verify_result.error,
207
+ )
208
+
209
+ verified_email = verify_result.email or ""
210
+ identity = AgentIdentity(email=verified_email)
211
+
212
+ try:
213
+ user = resolver(identity)
214
+ except Exception:
215
+ return AccountOutcome(
216
+ ok=False,
217
+ code="RESOLVER_ERROR",
218
+ error="The account resolver threw. This is a fault in the resolver, not the token.",
219
+ )
220
+
221
+ if user is not None:
222
+ return AccountOutcome(
223
+ ok=True,
224
+ email=verified_email,
225
+ user=user,
226
+ created=False,
227
+ )
228
+
229
+ try:
230
+ new_user = creator(identity)
231
+ except Exception:
232
+ return AccountOutcome(
233
+ ok=False,
234
+ code="RESOLVER_ERROR",
235
+ error="The account creator threw. This is a fault in the creator, not the token.",
236
+ )
237
+
238
+ return AccountOutcome(
239
+ ok=True,
240
+ email=verified_email,
241
+ user=new_user,
242
+ created=True,
243
+ )
244
+
245
+
246
+ async def create_agent_account_async(
247
+ token: str,
248
+ *,
249
+ audience: str,
250
+ resolver: AsyncAccountResolver[TUser],
251
+ creator: AsyncAccountCreator[TUser],
252
+ issuer: str | None = None,
253
+ jwks_url: str | None = None,
254
+ jwks: dict[str, Any] | None = None,
255
+ fetch_jwks: Callable[[str], dict[str, Any]] | None = None,
256
+ fetch_jwks_async: Callable[[str], Awaitable[dict[str, Any]]] | None = None,
257
+ clock_tolerance_seconds: int = DEFAULT_CLOCK_TOLERANCE_SECONDS,
258
+ ) -> AccountOutcome[TUser]:
259
+ """Asynchronous variant of create_agent_account."""
260
+ verify_result = await verify_agent_token_async(
261
+ token=token,
262
+ audience=audience,
263
+ issuer=issuer,
264
+ jwks_url=jwks_url,
265
+ jwks=jwks,
266
+ fetch_jwks=fetch_jwks,
267
+ fetch_jwks_async=fetch_jwks_async,
268
+ clock_tolerance_seconds=clock_tolerance_seconds,
269
+ )
270
+
271
+ if not verify_result.ok:
272
+ return AccountOutcome(
273
+ ok=False,
274
+ code=verify_result.code,
275
+ error=verify_result.error,
276
+ )
277
+
278
+ verified_email = verify_result.email or ""
279
+ identity = AgentIdentity(email=verified_email)
280
+
281
+ try:
282
+ user_val = resolver(identity)
283
+ if inspect.isawaitable(user_val):
284
+ user = await user_val
285
+ else:
286
+ user = user_val
287
+ except Exception:
288
+ return AccountOutcome(
289
+ ok=False,
290
+ code="RESOLVER_ERROR",
291
+ error="The account resolver threw. This is a fault in the resolver, not the token.",
292
+ )
293
+
294
+ if user is not None:
295
+ return AccountOutcome(
296
+ ok=True,
297
+ email=verified_email,
298
+ user=user,
299
+ created=False,
300
+ )
301
+
302
+ try:
303
+ new_user_val = creator(identity)
304
+ if inspect.isawaitable(new_user_val):
305
+ new_user = await new_user_val
306
+ else:
307
+ new_user = new_user_val
308
+ except Exception:
309
+ return AccountOutcome(
310
+ ok=False,
311
+ code="RESOLVER_ERROR",
312
+ error="The account creator threw. This is a fault in the creator, not the token.",
313
+ )
314
+
315
+ return AccountOutcome(
316
+ ok=True,
317
+ email=verified_email,
318
+ user=new_user,
319
+ created=True,
320
+ )
agentonboard/codes.py ADDED
@@ -0,0 +1,76 @@
1
+ """
2
+ Every failure verify_agent_token and verify_agent_account can report.
3
+
4
+ Published vocabulary: codes are only ever added, never renamed or removed, because partners
5
+ branch on them. Keep this list short — every code is a decision each language SDK has
6
+ to make identically (plans/0008-sdk-design.md §6).
7
+ """
8
+
9
+ from typing import Literal
10
+
11
+ VerifyErrorCode = Literal[
12
+ "EXPIRED",
13
+ "INVALID_SIGNATURE",
14
+ "AUDIENCE_MISMATCH",
15
+ "ISSUER_MISMATCH",
16
+ "MALFORMED_TOKEN",
17
+ "CONFIG_ERROR",
18
+ "UNKNOWN_KEY",
19
+ "KEY_SOURCE_UNAVAILABLE",
20
+ "MISSING_EMAIL",
21
+ ]
22
+
23
+ AccountErrorCode = Literal[
24
+ VerifyErrorCode,
25
+ "ACCOUNT_REQUIRED",
26
+ "RESOLVER_ERROR",
27
+ ]
28
+
29
+ VERIFY_ERROR_DESCRIPTIONS: dict[VerifyErrorCode, str] = {
30
+ "EXPIRED": "The token's exp claim is in the past, beyond the clock-skew tolerance.",
31
+ "INVALID_SIGNATURE": (
32
+ "The signature did not verify against a published key, or the token used an "
33
+ "algorithm this SDK does not accept."
34
+ ),
35
+ "AUDIENCE_MISMATCH": "The token was minted for a different audience, or carries no aud claim.",
36
+ "ISSUER_MISMATCH": "The token was minted by a different issuer, or carries no iss claim.",
37
+ "MALFORMED_TOKEN": (
38
+ "The token is not a readable JWT, or a claim this SDK requires is absent or unusable."
39
+ ),
40
+ "CONFIG_ERROR": "The call itself is wrong: audience, issuer, JWKS URL, or clock-skew tolerance.",
41
+ "UNKNOWN_KEY": (
42
+ "The token's kid matched no key in the key set this SDK could obtain. The code does "
43
+ "not say why: the key may be revoked or rotated away, the token may be forged, or "
44
+ "the key endpoint may be unreachable and leave this SDK holding an out-of-date set."
45
+ ),
46
+ "KEY_SOURCE_UNAVAILABLE": (
47
+ "This SDK could not obtain a signing key: the key set could not be fetched, the "
48
+ "published set is not one this SDK can verify against, or it publishes more than one "
49
+ "key for the token's kid."
50
+ ),
51
+ "MISSING_EMAIL": "The token verified but carries no usable email claim to identify the human.",
52
+ }
53
+
54
+ ACCOUNT_ERROR_DESCRIPTIONS: dict[AccountErrorCode, str] = {
55
+ **VERIFY_ERROR_DESCRIPTIONS, # type: ignore[dict-item]
56
+ "ACCOUNT_REQUIRED": (
57
+ "The token verified, but the partner resolver returned no account for this identity."
58
+ ),
59
+ "RESOLVER_ERROR": "The partner's account lookup or creation callback threw an error.",
60
+ }
61
+
62
+
63
+ def status_from_code(code: str | None) -> int:
64
+ """
65
+ Turns a verification or account failure code into an HTTP status code.
66
+ Rules per agent-token-v1 statusMapping:
67
+ - 403 for ACCOUNT_REQUIRED
68
+ - 500 for RESOLVER_ERROR and CONFIG_ERROR
69
+ - 401 for all other error codes
70
+ """
71
+ if code == "ACCOUNT_REQUIRED":
72
+ return 403
73
+ if code in ("RESOLVER_ERROR", "CONFIG_ERROR"):
74
+ return 500
75
+ return 401
76
+
@@ -0,0 +1,3 @@
1
+ """
2
+ Framework integrations for AgentOnboard.
3
+ """
@@ -0,0 +1,119 @@
1
+ """
2
+ FastAPI security dependency for AgentOnboard.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Generic, TypeVar
8
+
9
+ from fastapi import HTTPException, Request, status
10
+ from fastapi.openapi.models import HTTPBearer as HTTPBearerModel
11
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
12
+ from fastapi.security.base import SecurityBase
13
+
14
+ from agentonboard.account import (
15
+ AgentIdentity,
16
+ AsyncAccountCreator,
17
+ AsyncAccountResolver,
18
+ create_agent_account_async,
19
+ verify_agent_account_async,
20
+ )
21
+ from agentonboard.codes import status_from_code
22
+ from agentonboard.verify import verify_agent_token_async
23
+
24
+ TUser = TypeVar("TUser")
25
+
26
+
27
+ class AgentBearerAuth(SecurityBase, Generic[TUser]):
28
+ """
29
+ FastAPI dependency for verifying AgentOnboard session tokens and resolving accounts.
30
+
31
+ Usage:
32
+ auth = AgentBearerAuth(audience="notes.com")
33
+
34
+ @app.get("/items")
35
+ async def read_items(identity: AgentIdentity = Depends(auth)):
36
+ return {"user": identity.email}
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ *,
42
+ audience: str,
43
+ resolver: AsyncAccountResolver[TUser] | None = None,
44
+ creator: AsyncAccountCreator[TUser] | None = None,
45
+ auto_create: bool = False,
46
+ issuer: str | None = None,
47
+ jwks_url: str | None = None,
48
+ jwks: dict[str, Any] | None = None,
49
+ auto_error: bool = True,
50
+ ) -> None:
51
+ self.model = HTTPBearerModel()
52
+ self.scheme_name = self.__class__.__name__
53
+ self.bearer = HTTPBearer(auto_error=auto_error)
54
+ self.audience = audience
55
+ self.resolver = resolver
56
+ self.creator = creator
57
+ self.auto_create = auto_create
58
+ self.issuer = issuer
59
+ self.jwks_url = jwks_url
60
+ self.jwks = jwks
61
+
62
+ async def __call__(
63
+ self, request: Request
64
+ ) -> AgentIdentity | TUser:
65
+ credentials: HTTPAuthorizationCredentials | None = await self.bearer(request)
66
+ if not credentials or not credentials.credentials:
67
+ raise HTTPException(
68
+ status_code=status.HTTP_401_UNAUTHORIZED,
69
+ detail={"code": "MALFORMED_TOKEN", "error": "Missing authorization bearer token"},
70
+ )
71
+
72
+ token = credentials.credentials
73
+
74
+ if self.resolver is None:
75
+ # Token verification only
76
+ result = await verify_agent_token_async(
77
+ token=token,
78
+ audience=self.audience,
79
+ issuer=self.issuer,
80
+ jwks_url=self.jwks_url,
81
+ jwks=self.jwks,
82
+ )
83
+ if not result.ok:
84
+ http_status = status_from_code(result.code)
85
+ raise HTTPException(
86
+ status_code=http_status,
87
+ detail={"code": result.code, "error": result.error},
88
+ )
89
+ return AgentIdentity(email=result.email or "")
90
+
91
+ # Account resolution / provisioning
92
+ if self.auto_create and self.creator is not None:
93
+ outcome = await create_agent_account_async(
94
+ token=token,
95
+ audience=self.audience,
96
+ resolver=self.resolver,
97
+ creator=self.creator,
98
+ issuer=self.issuer,
99
+ jwks_url=self.jwks_url,
100
+ jwks=self.jwks,
101
+ )
102
+ else:
103
+ outcome = await verify_agent_account_async(
104
+ token=token,
105
+ audience=self.audience,
106
+ resolver=self.resolver,
107
+ issuer=self.issuer,
108
+ jwks_url=self.jwks_url,
109
+ jwks=self.jwks,
110
+ )
111
+
112
+ if not outcome.ok:
113
+ http_status = status_from_code(outcome.code)
114
+ raise HTTPException(
115
+ status_code=http_status,
116
+ detail={"code": outcome.code, "error": outcome.error},
117
+ )
118
+
119
+ return outcome.user # type: ignore[return-value]
@@ -0,0 +1,124 @@
1
+ """
2
+ Flask decorator and helper for AgentOnboard.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import functools
8
+ import inspect
9
+ from collections.abc import Callable
10
+ from typing import Any, TypeVar
11
+
12
+ from flask import g, jsonify, request
13
+
14
+ from agentonboard.account import (
15
+ AccountCreator,
16
+ AccountResolver,
17
+ AgentIdentity,
18
+ create_agent_account,
19
+ verify_agent_account,
20
+ )
21
+ from agentonboard.codes import status_from_code
22
+ from agentonboard.verify import verify_agent_token
23
+
24
+ F = TypeVar("F", bound=Callable[..., Any])
25
+ TUser = TypeVar("TUser")
26
+
27
+
28
+ def require_agent_token(
29
+ *,
30
+ audience: str,
31
+ resolver: AccountResolver[TUser] | None = None,
32
+ creator: AccountCreator[TUser] | None = None,
33
+ auto_create: bool = False,
34
+ issuer: str | None = None,
35
+ jwks_url: str | None = None,
36
+ jwks: dict[str, Any] | None = None,
37
+ ) -> Callable[[F], F]:
38
+ """
39
+ Flask route decorator for verifying AgentOnboard session tokens.
40
+
41
+ Usage:
42
+ @app.route("/protected")
43
+ @require_agent_token(audience="notes.com")
44
+ def protected():
45
+ return jsonify({"user": g.agent_identity.email})
46
+ """
47
+
48
+ def decorator(fn: F) -> F:
49
+ @functools.wraps(fn)
50
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
51
+ auth_header = request.headers.get("Authorization", "")
52
+ if not auth_header or not auth_header.startswith("Bearer "):
53
+ return (
54
+ jsonify(
55
+ {
56
+ "code": "MALFORMED_TOKEN",
57
+ "error": "Missing or malformed Authorization Bearer header",
58
+ }
59
+ ),
60
+ 401,
61
+ )
62
+
63
+ token = auth_header[7:].strip()
64
+
65
+ if resolver is None:
66
+ res = verify_agent_token(
67
+ token=token,
68
+ audience=audience,
69
+ issuer=issuer,
70
+ jwks_url=jwks_url,
71
+ jwks=jwks,
72
+ )
73
+ if not res.ok:
74
+ status_code = status_from_code(res.code)
75
+ return jsonify({"code": res.code, "error": res.error}), status_code
76
+
77
+ identity = AgentIdentity(email=res.email or "")
78
+ g.agent_identity = identity
79
+
80
+ sig = inspect.signature(fn)
81
+ if "agent_identity" in sig.parameters:
82
+ kwargs["agent_identity"] = identity
83
+ return fn(*args, **kwargs)
84
+
85
+ # Account resolution
86
+ if auto_create and creator is not None:
87
+ outcome = create_agent_account(
88
+ token=token,
89
+ audience=audience,
90
+ resolver=resolver,
91
+ creator=creator,
92
+ issuer=issuer,
93
+ jwks_url=jwks_url,
94
+ jwks=jwks,
95
+ )
96
+ else:
97
+ outcome = verify_agent_account(
98
+ token=token,
99
+ audience=audience,
100
+ resolver=resolver,
101
+ issuer=issuer,
102
+ jwks_url=jwks_url,
103
+ jwks=jwks,
104
+ )
105
+
106
+ if not outcome.ok:
107
+ status_code = status_from_code(outcome.code)
108
+ return jsonify({"code": outcome.code, "error": outcome.error}), status_code
109
+
110
+ identity = AgentIdentity(email=outcome.email or "")
111
+ g.agent_identity = identity
112
+ g.agent_user = outcome.user
113
+
114
+ sig = inspect.signature(fn)
115
+ if "agent_user" in sig.parameters:
116
+ kwargs["agent_user"] = outcome.user
117
+ if "agent_identity" in sig.parameters:
118
+ kwargs["agent_identity"] = identity
119
+
120
+ return fn(*args, **kwargs)
121
+
122
+ return wrapper # type: ignore[return-value]
123
+
124
+ return decorator