google-adk-extras 0.2.7__tar.gz → 0.3.1__tar.gz
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.
- {google_adk_extras-0.2.7/src/google_adk_extras.egg-info → google_adk_extras-0.3.1}/PKG-INFO +74 -2
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/README.md +73 -1
- google_adk_extras-0.3.1/docs/auth.md +79 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/docs/index.md +6 -1
- google_adk_extras-0.3.1/examples/adk_server_8015.py +43 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/mkdocs.yml +3 -2
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/pyproject.toml +1 -1
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/__init__.py +1 -1
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/adk_builder.py +1 -0
- google_adk_extras-0.3.1/src/google_adk_extras/auth/__init__.py +10 -0
- google_adk_extras-0.3.1/src/google_adk_extras/auth/attach.py +232 -0
- google_adk_extras-0.3.1/src/google_adk_extras/auth/config.py +45 -0
- google_adk_extras-0.3.1/src/google_adk_extras/auth/jwt_utils.py +45 -0
- google_adk_extras-0.3.1/src/google_adk_extras/auth/sql_store.py +183 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/enhanced_fastapi.py +53 -13
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1/src/google_adk_extras.egg-info}/PKG-INFO +74 -2
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras.egg-info/SOURCES.txt +7 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/LICENSE +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/MANIFEST.in +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/docs/agent-loading.md +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/docs/examples.md +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/docs/fastapi.md +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/docs/getting-started.md +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/docs/quickstarts.md +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/docs/services.md +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/docs/streaming.md +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/docs/troubleshooting.md +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/docs/uris.md +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/examples/README.md +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/examples/consume_remote_a2a.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/examples/custom_loader.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/examples/fastapi_app.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/examples/programmatic_a2a_expose.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/examples/runner_basic.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/examples/services/artifacts_local.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/examples/services/memory_yaml.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/examples/services/sessions_sql.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/examples/streaming_sse_ws.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/setup.cfg +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/setup.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/artifacts/__init__.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/artifacts/base_custom_artifact_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/artifacts/local_folder_artifact_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/artifacts/mongo_artifact_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/artifacts/s3_artifact_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/artifacts/sql_artifact_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/credentials/base_custom_credential_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/custom_agent_loader.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/enhanced_adk_web_server.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/enhanced_runner.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/memory/__init__.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/memory/base_custom_memory_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/memory/mongo_memory_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/memory/redis_memory_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/memory/sql_memory_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/memory/yaml_file_memory_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/sessions/__init__.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/sessions/base_custom_session_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/sessions/mongo_session_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/sessions/redis_session_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/sessions/sql_session_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/sessions/yaml_file_session_service.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/streaming/__init__.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras/streaming/streaming_controller.py +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras.egg-info/dependency_links.txt +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras.egg-info/requires.txt +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/src/google_adk_extras.egg-info/top_level.txt +0 -0
- {google_adk_extras-0.2.7 → google_adk_extras-0.3.1}/tests/test_a2a_helpers.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: google-adk-extras
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.1
|
4
4
|
Summary: Production-ready services and FastAPI wiring for Google ADK
|
5
5
|
Home-page: https://github.com/DeadMeme5441/google-adk-extras
|
6
6
|
Author: DeadMeme5441
|
@@ -94,7 +94,7 @@ If you plan to use specific backends, also install their clients (examples):
|
|
94
94
|
- Redis: `uv pip install redis`
|
95
95
|
- S3: `uv pip install boto3`
|
96
96
|
|
97
|
-
Note on credentials (0.
|
97
|
+
Note on credentials (0.3.0): Outbound credentials for tools remain ADK’s concern (use ADK’s BaseCredentialService). Inbound API authentication is now available as an optional FastAPI layer in this package (see Auth below). You can run fully open (no auth) or enable API Key, Basic, or JWT (including first‑party issuance backed by SQL).
|
98
98
|
|
99
99
|
|
100
100
|
## Quickstart (FastAPI)
|
@@ -104,6 +104,7 @@ Use the fluent builder to wire services. Then run with uvicorn.
|
|
104
104
|
```python
|
105
105
|
# app.py
|
106
106
|
from google_adk_extras import AdkBuilder
|
107
|
+
from google_adk_extras.auth import AuthConfig, JwtIssuerConfig, JwtValidatorConfig
|
107
108
|
|
108
109
|
app = (
|
109
110
|
AdkBuilder()
|
@@ -127,6 +128,77 @@ uvicorn app:app --reload
|
|
127
128
|
If you don’t keep agents on disk, register them programmatically and use a custom loader (see below).
|
128
129
|
|
129
130
|
|
131
|
+
## Auth (optional)
|
132
|
+
|
133
|
+
Auth is entirely optional. By default, all endpoints are open (no auth). To enable protection, pass `auth_config` into `get_enhanced_fast_api_app` via the builder or directly.
|
134
|
+
|
135
|
+
Supported inbound methods:
|
136
|
+
- API Key: `X-API-Key: <key>` header (or `?api_key=` query). Keys can be static via config, or issued/rotated via SQL‑backed endpoints.
|
137
|
+
- HTTP Basic: `Authorization: Basic base64(user:pass)` for quick human/internal testing. Can validate against in‑memory map or the SQL users table.
|
138
|
+
- Bearer JWT (validate): Accept JWTs from Google/Auth0/Okta/etc. via JWKS, or HS256 secret in dev. Enforces iss/aud/exp/nbf.
|
139
|
+
- Bearer JWT (issue): First‑party issuer with HS256, tokens minted from `/auth/token`, users stored in SQL (SQLite/Postgres/MySQL).
|
140
|
+
|
141
|
+
Minimal enablement (JWT validate only):
|
142
|
+
|
143
|
+
```python
|
144
|
+
from google_adk_extras.auth import AuthConfig, JwtValidatorConfig
|
145
|
+
|
146
|
+
auth = AuthConfig(
|
147
|
+
enabled=True,
|
148
|
+
jwt_validator=JwtValidatorConfig(
|
149
|
+
jwks_url="https://accounts.google.com/.well-known/openid-configuration", # example
|
150
|
+
issuer="https://accounts.google.com",
|
151
|
+
audience="your-api-audience",
|
152
|
+
),
|
153
|
+
)
|
154
|
+
|
155
|
+
app = (
|
156
|
+
AdkBuilder()
|
157
|
+
.with_agents_dir("./agents")
|
158
|
+
.build_fastapi_app()
|
159
|
+
)
|
160
|
+
```
|
161
|
+
|
162
|
+
First‑party issuer + validate (single shared HS256 secret) with SQL connector:
|
163
|
+
|
164
|
+
```python
|
165
|
+
from google_adk_extras.auth import AuthConfig, JwtIssuerConfig, JwtValidatorConfig
|
166
|
+
|
167
|
+
issuer = JwtIssuerConfig(
|
168
|
+
enabled=True,
|
169
|
+
issuer="https://local-issuer",
|
170
|
+
audience="adk-api",
|
171
|
+
algorithm="HS256",
|
172
|
+
hs256_secret="topsecret",
|
173
|
+
database_url="sqlite:///./auth.db", # also supports Postgres/MySQL
|
174
|
+
)
|
175
|
+
validator = JwtValidatorConfig(
|
176
|
+
issuer=issuer.issuer,
|
177
|
+
audience=issuer.audience,
|
178
|
+
hs256_secret=issuer.hs256_secret,
|
179
|
+
)
|
180
|
+
|
181
|
+
auth = AuthConfig(enabled=True, jwt_issuer=issuer, jwt_validator=validator)
|
182
|
+
|
183
|
+
app = (
|
184
|
+
AdkBuilder()
|
185
|
+
.with_agents_dir("./agents")
|
186
|
+
.build_fastapi_app()
|
187
|
+
)
|
188
|
+
```
|
189
|
+
|
190
|
+
Issuing and using tokens/keys at runtime:
|
191
|
+
- Register user: `POST /auth/register?username=alice&password=wonder`
|
192
|
+
- Token (password): `POST /auth/token?grant_type=password&username=alice&password=wonder`
|
193
|
+
- Refresh: `POST /auth/refresh?user_id=<uid>&refresh_token=<jti>`
|
194
|
+
- Create API key: `POST /auth/api-keys` (auth required) → returns `{ id, api_key }` (plaintext shown once)
|
195
|
+
- List keys: `GET /auth/api-keys` (auth required)
|
196
|
+
- Revoke key: `DELETE /auth/api-keys/{id}` (auth required)
|
197
|
+
- Use API key: add `X-API-Key: <api_key>` to any protected route (keys currently allow full access)
|
198
|
+
|
199
|
+
Protected routes include `/run`, `/run_sse`, all `/apps/...` session/artifact/eval endpoints, `/debug/*`, `/builder/*`, and optionally `/list-apps` and `/apps/{app}/metrics-info`.
|
200
|
+
|
201
|
+
|
130
202
|
## Quickstart (Runner)
|
131
203
|
|
132
204
|
Create a Runner wired with your chosen backends. Use agent name (filesystem loader) or pass an agent instance.
|
@@ -51,7 +51,7 @@ If you plan to use specific backends, also install their clients (examples):
|
|
51
51
|
- Redis: `uv pip install redis`
|
52
52
|
- S3: `uv pip install boto3`
|
53
53
|
|
54
|
-
Note on credentials (0.
|
54
|
+
Note on credentials (0.3.0): Outbound credentials for tools remain ADK’s concern (use ADK’s BaseCredentialService). Inbound API authentication is now available as an optional FastAPI layer in this package (see Auth below). You can run fully open (no auth) or enable API Key, Basic, or JWT (including first‑party issuance backed by SQL).
|
55
55
|
|
56
56
|
|
57
57
|
## Quickstart (FastAPI)
|
@@ -61,6 +61,7 @@ Use the fluent builder to wire services. Then run with uvicorn.
|
|
61
61
|
```python
|
62
62
|
# app.py
|
63
63
|
from google_adk_extras import AdkBuilder
|
64
|
+
from google_adk_extras.auth import AuthConfig, JwtIssuerConfig, JwtValidatorConfig
|
64
65
|
|
65
66
|
app = (
|
66
67
|
AdkBuilder()
|
@@ -84,6 +85,77 @@ uvicorn app:app --reload
|
|
84
85
|
If you don’t keep agents on disk, register them programmatically and use a custom loader (see below).
|
85
86
|
|
86
87
|
|
88
|
+
## Auth (optional)
|
89
|
+
|
90
|
+
Auth is entirely optional. By default, all endpoints are open (no auth). To enable protection, pass `auth_config` into `get_enhanced_fast_api_app` via the builder or directly.
|
91
|
+
|
92
|
+
Supported inbound methods:
|
93
|
+
- API Key: `X-API-Key: <key>` header (or `?api_key=` query). Keys can be static via config, or issued/rotated via SQL‑backed endpoints.
|
94
|
+
- HTTP Basic: `Authorization: Basic base64(user:pass)` for quick human/internal testing. Can validate against in‑memory map or the SQL users table.
|
95
|
+
- Bearer JWT (validate): Accept JWTs from Google/Auth0/Okta/etc. via JWKS, or HS256 secret in dev. Enforces iss/aud/exp/nbf.
|
96
|
+
- Bearer JWT (issue): First‑party issuer with HS256, tokens minted from `/auth/token`, users stored in SQL (SQLite/Postgres/MySQL).
|
97
|
+
|
98
|
+
Minimal enablement (JWT validate only):
|
99
|
+
|
100
|
+
```python
|
101
|
+
from google_adk_extras.auth import AuthConfig, JwtValidatorConfig
|
102
|
+
|
103
|
+
auth = AuthConfig(
|
104
|
+
enabled=True,
|
105
|
+
jwt_validator=JwtValidatorConfig(
|
106
|
+
jwks_url="https://accounts.google.com/.well-known/openid-configuration", # example
|
107
|
+
issuer="https://accounts.google.com",
|
108
|
+
audience="your-api-audience",
|
109
|
+
),
|
110
|
+
)
|
111
|
+
|
112
|
+
app = (
|
113
|
+
AdkBuilder()
|
114
|
+
.with_agents_dir("./agents")
|
115
|
+
.build_fastapi_app()
|
116
|
+
)
|
117
|
+
```
|
118
|
+
|
119
|
+
First‑party issuer + validate (single shared HS256 secret) with SQL connector:
|
120
|
+
|
121
|
+
```python
|
122
|
+
from google_adk_extras.auth import AuthConfig, JwtIssuerConfig, JwtValidatorConfig
|
123
|
+
|
124
|
+
issuer = JwtIssuerConfig(
|
125
|
+
enabled=True,
|
126
|
+
issuer="https://local-issuer",
|
127
|
+
audience="adk-api",
|
128
|
+
algorithm="HS256",
|
129
|
+
hs256_secret="topsecret",
|
130
|
+
database_url="sqlite:///./auth.db", # also supports Postgres/MySQL
|
131
|
+
)
|
132
|
+
validator = JwtValidatorConfig(
|
133
|
+
issuer=issuer.issuer,
|
134
|
+
audience=issuer.audience,
|
135
|
+
hs256_secret=issuer.hs256_secret,
|
136
|
+
)
|
137
|
+
|
138
|
+
auth = AuthConfig(enabled=True, jwt_issuer=issuer, jwt_validator=validator)
|
139
|
+
|
140
|
+
app = (
|
141
|
+
AdkBuilder()
|
142
|
+
.with_agents_dir("./agents")
|
143
|
+
.build_fastapi_app()
|
144
|
+
)
|
145
|
+
```
|
146
|
+
|
147
|
+
Issuing and using tokens/keys at runtime:
|
148
|
+
- Register user: `POST /auth/register?username=alice&password=wonder`
|
149
|
+
- Token (password): `POST /auth/token?grant_type=password&username=alice&password=wonder`
|
150
|
+
- Refresh: `POST /auth/refresh?user_id=<uid>&refresh_token=<jti>`
|
151
|
+
- Create API key: `POST /auth/api-keys` (auth required) → returns `{ id, api_key }` (plaintext shown once)
|
152
|
+
- List keys: `GET /auth/api-keys` (auth required)
|
153
|
+
- Revoke key: `DELETE /auth/api-keys/{id}` (auth required)
|
154
|
+
- Use API key: add `X-API-Key: <api_key>` to any protected route (keys currently allow full access)
|
155
|
+
|
156
|
+
Protected routes include `/run`, `/run_sse`, all `/apps/...` session/artifact/eval endpoints, `/debug/*`, `/builder/*`, and optionally `/list-apps` and `/apps/{app}/metrics-info`.
|
157
|
+
|
158
|
+
|
87
159
|
## Quickstart (Runner)
|
88
160
|
|
89
161
|
Create a Runner wired with your chosen backends. Use agent name (filesystem loader) or pass an agent instance.
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# Auth (Optional)
|
2
|
+
|
3
|
+
Inbound API authentication is optional. If you don’t pass an `auth_config`, all routes are open (useful for local dev). When enabled, the middleware protects sensitive routes and enforces identity where appropriate.
|
4
|
+
|
5
|
+
Supported methods:
|
6
|
+
- API Key
|
7
|
+
- Send `X-API-Key: <token>` header or `?api_key=<token>` query.
|
8
|
+
- Keys can be static via config or issued/rotated via SQL‑backed endpoints.
|
9
|
+
- HTTP Basic
|
10
|
+
- `Authorization: Basic base64(user:pass)`.
|
11
|
+
- Checks an in‑memory map or the SQL users table when configured.
|
12
|
+
- Bearer JWT (validate)
|
13
|
+
- Validate JWTs from OIDC providers (Google, Auth0, Okta, etc.) via JWKS.
|
14
|
+
- Or use HS256 with a shared secret in dev.
|
15
|
+
- Bearer JWT (issue)
|
16
|
+
- First‑party issuer backed by SQL; exposes `/auth/register`, `/auth/token`, `/auth/refresh`.
|
17
|
+
|
18
|
+
## Quick examples
|
19
|
+
|
20
|
+
### Enable JWT validate only
|
21
|
+
```python
|
22
|
+
from google_adk_extras import AdkBuilder
|
23
|
+
from google_adk_extras.auth import AuthConfig, JwtValidatorConfig
|
24
|
+
|
25
|
+
auth = AuthConfig(
|
26
|
+
enabled=True,
|
27
|
+
jwt_validator=JwtValidatorConfig(
|
28
|
+
jwks_url="https://YOUR_ISSUER/.well-known/jwks.json",
|
29
|
+
issuer="https://YOUR_ISSUER",
|
30
|
+
audience="your-api-audience",
|
31
|
+
),
|
32
|
+
)
|
33
|
+
|
34
|
+
app = AdkBuilder().with_agents_dir("./agents").build_fastapi_app()
|
35
|
+
```
|
36
|
+
|
37
|
+
### First‑party issuer + validate (HS256) and SQL store
|
38
|
+
```python
|
39
|
+
from google_adk_extras import AdkBuilder
|
40
|
+
from google_adk_extras.auth import AuthConfig, JwtIssuerConfig, JwtValidatorConfig
|
41
|
+
|
42
|
+
issuer = JwtIssuerConfig(
|
43
|
+
enabled=True,
|
44
|
+
issuer="https://local-issuer",
|
45
|
+
audience="adk-api",
|
46
|
+
algorithm="HS256",
|
47
|
+
hs256_secret="topsecret",
|
48
|
+
database_url="sqlite:///./auth.db",
|
49
|
+
)
|
50
|
+
validator = JwtValidatorConfig(issuer=issuer.issuer, audience=issuer.audience, hs256_secret=issuer.hs256_secret)
|
51
|
+
|
52
|
+
auth = AuthConfig(enabled=True, jwt_issuer=issuer, jwt_validator=validator)
|
53
|
+
|
54
|
+
app = AdkBuilder().with_agents_dir("./agents").build_fastapi_app()
|
55
|
+
```
|
56
|
+
|
57
|
+
### API key management endpoints (SQL store)
|
58
|
+
- `POST /auth/api-keys` → `{ id, api_key }` (plaintext shown once)
|
59
|
+
- `GET /auth/api-keys` → list metadata
|
60
|
+
- `DELETE /auth/api-keys/{id}` → revoke
|
61
|
+
|
62
|
+
Use `X-API-Key: <api_key>` (or `?api_key=`) to access protected routes.
|
63
|
+
|
64
|
+
## What’s protected
|
65
|
+
- Always: `POST /run`, `POST /run_sse`, `GET/POST/DELETE /apps/...`, `/debug/*`, `/builder/*`.
|
66
|
+
- Optional: `/list-apps` and `/apps/{app}/metrics-info` (toggled in `AuthConfig`).
|
67
|
+
- Ownership: when the URL contains `/users/{user_id}/...`, the `sub` from the token must match `user_id` (API key bypass permitted).
|
68
|
+
|
69
|
+
## Optional by design
|
70
|
+
- No‑auth is the default. Enable auth only when you’re ready.
|
71
|
+
- You can mix modes: e.g., JWT for users and API keys for automation.
|
72
|
+
|
73
|
+
```python
|
74
|
+
# Direct call if not using the builder
|
75
|
+
from google_adk_extras.enhanced_fastapi import get_enhanced_fast_api_app
|
76
|
+
from google_adk_extras.auth import AuthConfig
|
77
|
+
app = get_enhanced_fast_api_app(..., auth_config=AuthConfig(enabled=True, api_keys=["test"]))
|
78
|
+
```
|
79
|
+
|
@@ -14,6 +14,11 @@ What this is not: a fork of ADK. It builds on top of google-adk.
|
|
14
14
|
- `EnhancedAdkWebServer`
|
15
15
|
- `EnhancedRunner` (thin wrapper)
|
16
16
|
- `CustomAgentLoader` (programmatic agents)
|
17
|
-
- Services via subpackages: `sessions`, `artifacts`, `memory
|
17
|
+
- Services via subpackages: `sessions`, `artifacts`, `memory` (optional inbound auth lives under `auth/`)
|
18
18
|
|
19
19
|
See Quickstarts for copy‑paste examples.
|
20
|
+
|
21
|
+
Additional guides:
|
22
|
+
- [FastAPI Integration](fastapi.md)
|
23
|
+
- [Streaming](streaming.md)
|
24
|
+
- [Auth (Optional)](auth.md)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
"""Spin up an ADK FastAPI server on 127.0.0.1:8015 with in-memory services.
|
2
|
+
|
3
|
+
This script uses google_adk_extras.get_enhanced_fast_api_app with a simple
|
4
|
+
programmatic agent loader. It is intended only to inspect the OpenAPI schema
|
5
|
+
and enumerate endpoints exposed by the ADK web server.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import AsyncGenerator, Optional
|
9
|
+
|
10
|
+
from fastapi import FastAPI
|
11
|
+
|
12
|
+
from google.genai import types
|
13
|
+
from google.adk.events.event import Event
|
14
|
+
from google.adk.agents.base_agent import BaseAgent
|
15
|
+
|
16
|
+
from google_adk_extras.custom_agent_loader import CustomAgentLoader
|
17
|
+
from google_adk_extras.enhanced_fastapi import get_enhanced_fast_api_app
|
18
|
+
|
19
|
+
|
20
|
+
class _DummyAgent(BaseAgent):
|
21
|
+
"""Minimal agent; not used for OpenAPI generation, but loadable if needed."""
|
22
|
+
|
23
|
+
def __init__(self, name: str = "dummy"):
|
24
|
+
super().__init__(name)
|
25
|
+
|
26
|
+
async def run_async(self, ctx) -> AsyncGenerator[Event, None]:
|
27
|
+
# Emit a trivial final event; unlikely to be executed in this script.
|
28
|
+
content = types.Content(parts=[types.Part(text="ok")])
|
29
|
+
yield Event(author=self.name, content=content)
|
30
|
+
|
31
|
+
|
32
|
+
def build_app() -> FastAPI:
|
33
|
+
loader = CustomAgentLoader()
|
34
|
+
app = get_enhanced_fast_api_app(
|
35
|
+
agent_loader=loader,
|
36
|
+
web=False, # no static UI
|
37
|
+
enable_streaming=False, # focus on ADK core endpoints
|
38
|
+
allow_origins=["*"]
|
39
|
+
)
|
40
|
+
return app
|
41
|
+
|
42
|
+
|
43
|
+
app = build_app()
|
@@ -14,6 +14,7 @@ nav:
|
|
14
14
|
- Getting Started: getting-started.md
|
15
15
|
- Quickstarts: quickstarts.md
|
16
16
|
- FastAPI Integration: fastapi.md
|
17
|
+
- Auth (Optional): auth.md
|
17
18
|
- Streaming: streaming.md
|
18
19
|
- Services:
|
19
20
|
- Durable Services: services.md
|
@@ -46,10 +47,10 @@ plugins:
|
|
46
47
|
- quickstarts.md: A few end-to-end snippets to copy/paste
|
47
48
|
FastAPI & A2A:
|
48
49
|
- fastapi.md: Build a FastAPI app with AdkBuilder and enable A2A
|
50
|
+
Auth:
|
51
|
+
- auth.md: Optional inbound API authentication
|
49
52
|
Durable Services:
|
50
53
|
- services.md: Sessions, artifacts, memory backends and trade-offs
|
51
|
-
Credentials:
|
52
|
-
# credentials docs removed; use ADK documentation for outbound creds
|
53
54
|
Programmatic Agents:
|
54
55
|
- agent-loading.md: Custom loaders and in-memory registrations
|
55
56
|
Configuration:
|
@@ -683,6 +683,7 @@ class AdkBuilder:
|
|
683
683
|
eval_storage_uri=self._eval_storage_uri,
|
684
684
|
allow_origins=self._allow_origins,
|
685
685
|
web=self._web_ui,
|
686
|
+
# Expose future override via builder when needed
|
686
687
|
a2a=self._a2a,
|
687
688
|
programmatic_a2a=self._a2a_expose_programmatic,
|
688
689
|
programmatic_a2a_mount_base=self._a2a_programmatic_mount_base,
|
@@ -0,0 +1,232 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Optional
|
4
|
+
|
5
|
+
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
|
6
|
+
import base64
|
7
|
+
from fastapi.security import APIKeyHeader
|
8
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
9
|
+
|
10
|
+
from .config import AuthConfig, JwtIssuerConfig, JwtValidatorConfig
|
11
|
+
from .jwt_utils import decode_jwt, encode_jwt, now_ts
|
12
|
+
from typing import Any
|
13
|
+
|
14
|
+
|
15
|
+
def attach_auth(app: FastAPI, cfg: Optional[AuthConfig]) -> None:
|
16
|
+
"""Attach optional auth to the provided FastAPI app.
|
17
|
+
|
18
|
+
- Adds middleware that enforces auth on sensitive routes.
|
19
|
+
- Optionally registers token issuance endpoints if configured.
|
20
|
+
"""
|
21
|
+
if not cfg or not cfg.enabled or cfg.allow_no_auth:
|
22
|
+
return
|
23
|
+
|
24
|
+
validator = cfg.jwt_validator
|
25
|
+
issuer_cfg = cfg.jwt_issuer
|
26
|
+
api_keys = set(cfg.api_keys or [])
|
27
|
+
basic_users = cfg.basic_users or {}
|
28
|
+
auth_store: Optional[Any] = None
|
29
|
+
if issuer_cfg and issuer_cfg.database_url:
|
30
|
+
try:
|
31
|
+
from .sql_store import AuthStore # type: ignore
|
32
|
+
auth_store = AuthStore(issuer_cfg.database_url)
|
33
|
+
except Exception:
|
34
|
+
# SQL store not available; API key issuance and password grants will be unavailable.
|
35
|
+
auth_store = None
|
36
|
+
|
37
|
+
# Security helpers
|
38
|
+
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
|
39
|
+
|
40
|
+
async def _authenticate(request: Request) -> dict:
|
41
|
+
# API Key
|
42
|
+
api_key = request.query_params.get("api_key") or request.headers.get("x-api-key") or request.headers.get("X-API-Key")
|
43
|
+
if not api_key:
|
44
|
+
api_key = await api_key_header.__call__(request)
|
45
|
+
if api_key and api_key in api_keys:
|
46
|
+
return {"method": "api_key", "sub": "api_key_client"}
|
47
|
+
if api_key and auth_store and auth_store.verify_api_key(api_key):
|
48
|
+
return {"method": "api_key", "sub": "api_key_client"}
|
49
|
+
|
50
|
+
# Basic
|
51
|
+
authz = request.headers.get("authorization") or request.headers.get("Authorization")
|
52
|
+
if authz and authz.lower().startswith("basic "):
|
53
|
+
try:
|
54
|
+
b64 = authz.split(" ", 1)[1]
|
55
|
+
raw = base64.b64decode(b64).decode("utf-8")
|
56
|
+
username, _, password = raw.partition(":")
|
57
|
+
except Exception:
|
58
|
+
username, password = "", ""
|
59
|
+
# If SQL store present, try it first; else fall back to configured map
|
60
|
+
if auth_store:
|
61
|
+
uid = auth_store.authenticate_basic(username, password)
|
62
|
+
if uid:
|
63
|
+
return {"method": "basic", "sub": uid, "username": username}
|
64
|
+
stored = basic_users.get(username)
|
65
|
+
if stored and (stored == password):
|
66
|
+
return {"method": "basic", "sub": username, "username": username}
|
67
|
+
|
68
|
+
# Bearer JWT
|
69
|
+
if authz and authz.lower().startswith("bearer "):
|
70
|
+
token = authz.split(" ", 1)[1]
|
71
|
+
if validator and (validator.jwks_url or validator.hs256_secret):
|
72
|
+
try:
|
73
|
+
claims = decode_jwt(
|
74
|
+
token,
|
75
|
+
issuer=validator.issuer,
|
76
|
+
audience=validator.audience,
|
77
|
+
jwks_url=validator.jwks_url,
|
78
|
+
hs256_secret=validator.hs256_secret,
|
79
|
+
)
|
80
|
+
sub = str(claims.get("sub"))
|
81
|
+
if not sub:
|
82
|
+
raise HTTPException(status_code=401, detail="Invalid token: no subject")
|
83
|
+
return {"method": "jwt", "sub": sub, "claims": claims}
|
84
|
+
except Exception as e:
|
85
|
+
raise HTTPException(status_code=401, detail=f"Invalid token: {e}")
|
86
|
+
|
87
|
+
raise HTTPException(status_code=401, detail="Unauthorized")
|
88
|
+
|
89
|
+
def _path_requires_auth(path: str, method: str) -> bool:
|
90
|
+
method = method.upper()
|
91
|
+
# Always protect core run endpoints
|
92
|
+
if path == "/run" and method == "POST":
|
93
|
+
return True
|
94
|
+
if path == "/run_sse" and method == "POST":
|
95
|
+
return True
|
96
|
+
# Sessions and artifacts under /apps
|
97
|
+
if path.startswith("/apps/"):
|
98
|
+
# Allow metrics to be toggled
|
99
|
+
if path.endswith("/metrics-info") and method == "GET":
|
100
|
+
return cfg.protect_metrics
|
101
|
+
return True
|
102
|
+
# Debug and builder are privileged
|
103
|
+
if path.startswith("/debug/") or path.startswith("/builder/"):
|
104
|
+
return True
|
105
|
+
# API key management endpoints
|
106
|
+
if path.startswith("/auth/api-keys"):
|
107
|
+
return True
|
108
|
+
# Optionally protect list-apps
|
109
|
+
if path == "/list-apps" and method == "GET":
|
110
|
+
return cfg.protect_list_apps
|
111
|
+
return False
|
112
|
+
|
113
|
+
class _AuthMiddleware(BaseHTTPMiddleware):
|
114
|
+
async def dispatch(self, request: Request, call_next):
|
115
|
+
path = request.url.path
|
116
|
+
if not _path_requires_auth(path, request.method):
|
117
|
+
return await call_next(request)
|
118
|
+
# Authenticate
|
119
|
+
try:
|
120
|
+
request.state.identity = await _authenticate(request)
|
121
|
+
except HTTPException as e:
|
122
|
+
from fastapi.responses import JSONResponse
|
123
|
+
return JSONResponse({"detail": e.detail}, status_code=e.status_code)
|
124
|
+
# Optional: Enforce user ownership when path has /users/{user_id}/
|
125
|
+
try:
|
126
|
+
parts = path.strip("/").split("/")
|
127
|
+
if "users" in parts:
|
128
|
+
idx = parts.index("users")
|
129
|
+
claimed = parts[idx + 1]
|
130
|
+
sub = str(request.state.identity.get("sub"))
|
131
|
+
# Allow api_key method to bypass ownership
|
132
|
+
if request.state.identity.get("method") != "api_key" and sub != claimed:
|
133
|
+
from fastapi.responses import JSONResponse
|
134
|
+
return JSONResponse({"detail": "Forbidden: user mismatch"}, status_code=403)
|
135
|
+
except HTTPException:
|
136
|
+
raise
|
137
|
+
except Exception:
|
138
|
+
pass
|
139
|
+
return await call_next(request)
|
140
|
+
|
141
|
+
app.add_middleware(_AuthMiddleware)
|
142
|
+
|
143
|
+
# Token issuance endpoints (optional)
|
144
|
+
if issuer_cfg and issuer_cfg.enabled:
|
145
|
+
if issuer_cfg.algorithm == "HS256" and not issuer_cfg.hs256_secret:
|
146
|
+
raise RuntimeError("HS256 issuer requires hs256_secret")
|
147
|
+
router = APIRouter()
|
148
|
+
|
149
|
+
@router.post("/auth/register")
|
150
|
+
async def register(username: str, password: str):
|
151
|
+
if not auth_store:
|
152
|
+
raise HTTPException(status_code=400, detail="SQL store not configured")
|
153
|
+
uid = auth_store.create_user(username, password)
|
154
|
+
return {"user_id": uid}
|
155
|
+
|
156
|
+
@router.post("/auth/token")
|
157
|
+
async def token_grant(grant_type: str = "password", username: Optional[str] = None, password: Optional[str] = None,
|
158
|
+
user_id: Optional[str] = None, fingerprint: Optional[str] = None):
|
159
|
+
sub: Optional[str] = None
|
160
|
+
if grant_type == "password":
|
161
|
+
if not auth_store or not username or password is None:
|
162
|
+
raise HTTPException(status_code=400, detail="invalid_request")
|
163
|
+
uid = auth_store.authenticate_basic(username, password)
|
164
|
+
if not uid:
|
165
|
+
raise HTTPException(status_code=401, detail="invalid_grant")
|
166
|
+
sub = uid
|
167
|
+
elif grant_type == "client_credentials":
|
168
|
+
# For simplicity map to provided user_id
|
169
|
+
if not user_id:
|
170
|
+
raise HTTPException(status_code=400, detail="invalid_request")
|
171
|
+
sub = user_id
|
172
|
+
else:
|
173
|
+
raise HTTPException(status_code=400, detail="unsupported_grant_type")
|
174
|
+
|
175
|
+
now = now_ts()
|
176
|
+
access = {
|
177
|
+
"iss": issuer_cfg.issuer,
|
178
|
+
"aud": issuer_cfg.audience,
|
179
|
+
"sub": sub,
|
180
|
+
"iat": now,
|
181
|
+
"nbf": now,
|
182
|
+
"exp": now + issuer_cfg.access_ttl_seconds,
|
183
|
+
}
|
184
|
+
key = issuer_cfg.hs256_secret if issuer_cfg.algorithm == "HS256" else ""
|
185
|
+
access_token = encode_jwt(access, algorithm=issuer_cfg.algorithm, key=key)
|
186
|
+
|
187
|
+
refresh_token = None
|
188
|
+
if auth_store:
|
189
|
+
jti = auth_store.issue_refresh(sub, issuer_cfg.refresh_ttl_seconds, fingerprint=fingerprint)
|
190
|
+
refresh_token = jti
|
191
|
+
return {"access_token": access_token, "token_type": "bearer", "refresh_token": refresh_token}
|
192
|
+
|
193
|
+
@router.post("/auth/refresh")
|
194
|
+
async def refresh(user_id: str, refresh_token: str, fingerprint: Optional[str] = None):
|
195
|
+
if not auth_store:
|
196
|
+
raise HTTPException(status_code=400, detail="invalid_request")
|
197
|
+
if not auth_store.verify_refresh(refresh_token, user_id, fingerprint=fingerprint):
|
198
|
+
raise HTTPException(status_code=401, detail="invalid_grant")
|
199
|
+
now = now_ts()
|
200
|
+
access = {
|
201
|
+
"iss": issuer_cfg.issuer,
|
202
|
+
"aud": issuer_cfg.audience,
|
203
|
+
"sub": user_id,
|
204
|
+
"iat": now,
|
205
|
+
"nbf": now,
|
206
|
+
"exp": now + issuer_cfg.access_ttl_seconds,
|
207
|
+
}
|
208
|
+
key = issuer_cfg.hs256_secret if issuer_cfg.algorithm == "HS256" else ""
|
209
|
+
access_token = encode_jwt(access, algorithm=issuer_cfg.algorithm, key=key)
|
210
|
+
return {"access_token": access_token, "token_type": "bearer"}
|
211
|
+
|
212
|
+
app.include_router(router)
|
213
|
+
|
214
|
+
# API key management endpoints (require SQL store)
|
215
|
+
if auth_store:
|
216
|
+
api_router = APIRouter()
|
217
|
+
|
218
|
+
@api_router.post("/auth/api-keys")
|
219
|
+
async def create_api_key(user_id: Optional[str] = None, name: Optional[str] = None):
|
220
|
+
key_id, key_plain = auth_store.create_api_key(user_id=user_id, name=name)
|
221
|
+
return {"id": key_id, "api_key": key_plain}
|
222
|
+
|
223
|
+
@api_router.get("/auth/api-keys")
|
224
|
+
async def list_api_keys():
|
225
|
+
return auth_store.list_api_keys()
|
226
|
+
|
227
|
+
@api_router.delete("/auth/api-keys/{key_id}")
|
228
|
+
async def delete_api_key(key_id: str):
|
229
|
+
auth_store.revoke_api_key(key_id)
|
230
|
+
return {"ok": True}
|
231
|
+
|
232
|
+
app.include_router(api_router)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
from typing import List, Optional
|
5
|
+
|
6
|
+
|
7
|
+
@dataclass
|
8
|
+
class JwtValidatorConfig:
|
9
|
+
# Accept JWTs from external issuers (e.g., Google/Auth0/Okta) or our own issuer
|
10
|
+
jwks_url: Optional[str] = None
|
11
|
+
issuer: Optional[str] = None
|
12
|
+
audience: Optional[str] = None
|
13
|
+
# If you want to validate with an HS256 shared secret (tests/dev)
|
14
|
+
hs256_secret: Optional[str] = None
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class JwtIssuerConfig:
|
19
|
+
# Configure our own issuer if we issue tokens
|
20
|
+
enabled: bool = False
|
21
|
+
issuer: str = "https://example-issuer"
|
22
|
+
audience: str = "adk-api"
|
23
|
+
algorithm: str = "HS256" # HS256 or RS256/ES256 later
|
24
|
+
hs256_secret: Optional[str] = None
|
25
|
+
access_ttl_seconds: int = 3600
|
26
|
+
refresh_ttl_seconds: int = 60 * 60 * 24 * 14
|
27
|
+
# SQL store for users/refresh tokens
|
28
|
+
database_url: Optional[str] = None # e.g. sqlite:///auth.db
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass
|
32
|
+
class AuthConfig:
|
33
|
+
# Global toggle
|
34
|
+
enabled: bool = False
|
35
|
+
# Modes
|
36
|
+
allow_no_auth: bool = False # if True, bypass checks entirely
|
37
|
+
api_keys: List[str] = field(default_factory=list) # accepted API keys
|
38
|
+
basic_users: dict[str, str] = field(default_factory=dict) # username -> password (PBKDF2 hash or plaintext for tests)
|
39
|
+
jwt_validator: Optional[JwtValidatorConfig] = None
|
40
|
+
jwt_issuer: Optional[JwtIssuerConfig] = None
|
41
|
+
# Route policy toggles
|
42
|
+
protect_list_apps: bool = True
|
43
|
+
protect_metrics: bool = True
|
44
|
+
# Scopes are advisory; we currently validate presence of a token and subject. Extend as needed.
|
45
|
+
|