Commit cc304e0
fix(templates): unwrap SecretStr only on persistence path, not API responses
PR #744 stopped the SecretStr-not-JSON-serializable crash on
POST/PUT /templates by switching the configurations dump to
mode="json". That stops the crash, but Pydantic's default JSON
serialization for SecretStr is the masked form ("**********") —
silently corrupting the persisted token. At call time the loaded
template then sends `Authorization: Bearer **********` to the
upstream MCP / HTTP endpoint, which fails authentication.
Fix is in two parts:
1. HttpAuthConfig.field_serializer (template/types.py)
Reveals the underlying string for the three SecretStr fields
(token, password, api_key_value) **only when** model_dump is
invoked with context={"reveal_secrets": True}. Without the
context flag the serializer falls back to the masked
"**********" form. So:
- Persistence paths (create_template_handler,
replace_template_handler) pass the context and write the real
value (typically a `{credential_name}` placeholder resolved
per call from the credentials table).
- All other JSON serialization paths — notably FastAPI response
encoding for GET /templates/{id} and the PUT response — see
no context, fall through to the masked form, and so do not
leak literal tokens that an operator may have embedded
directly. This addresses the AI review note on the original
PR (mask_template_secrets() only masks template.secrets, not
template.configurations, so the Pydantic-level mask was the
only guard for configurations.mcp.servers[*].auth.token in
API responses).
2. template/cache.py — also passes reveal_secrets context
The Redis L2 template cache calls model_dump_json() to populate
itself. Without the context flag it would serialize the auth
token as "**********" and on cache HIT reconstruct the template
with auth.token = SecretStr("**********") — runtime would then
send `Authorization: Bearer **********` to the MCP server.
Reproduced live in chat: turn 1 (cache miss) succeeded because
MCP setup ran before the cache write, but turn 2+ hit the
poisoned cache and returned `web_search_exa error (401):
Invalid API key` from Exa. The cache is internal/private; the
masking only belongs in API responses, where the lack of context
preserves it.
mode="python" (the default) keeps the SecretStr instance, so
in-memory access via `.get_secret_value()` on the runtime path is
unaffected.
Verified end-to-end against the running local server, six cases:
Persistence + API response (HTTP layer)
1. Placeholder template POST -> DB has "{exa_api_key}" literal,
GET API response auth.token = "**********".
2. Literal-token template POST with token="sk-LITERAL-LEAK-CANARY-12345"
-> DB has the literal canary, GET API response auth.token =
"**********", canary string occurs zero times in the response
body (full-body grep).
3. PUT replace path: PUT with token="sk-PUT-CANARY-67890" -> DB
has the new literal canary, PUT response masked, canary occurs
zero times in response body.
4. Test rows cleaned up.
Redis cache + chat runtime (cache.py fix)
5. Busted local Redis cache for chat-direct-claude template, ran
two-turn chat session via the demo session API. Both turns
show `auth-fingerprint=resolved-len=43-tail=0556fe` (the real
Exa key is 36 chars, so "Bearer " + 36 = 43; tail matches the
real key suffix). Cache HIT (turn 2) preserves the placeholder.
6. Turn 2 prompt asked the LLM to web-search Strait of Hormuz
news; web_search_exa was invoked, returned real Reuters /
India trade news content with 7 cited URLs in the assistant
reply; zero `Invalid API key` strings in the response.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 7bd7f0f commit cc304e0
3 files changed
Lines changed: 54 additions & 9 deletions
File tree
- app
- ai/voice/agents/breeze_buddy/template
- api/routers/breeze_buddy/templates
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
66 | 75 | | |
67 | 76 | | |
68 | | - | |
| 77 | + | |
69 | 78 | | |
70 | 79 | | |
71 | 80 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
| 14 | + | |
13 | 15 | | |
14 | 16 | | |
15 | 17 | | |
| |||
1050 | 1052 | | |
1051 | 1053 | | |
1052 | 1054 | | |
| 1055 | + | |
| 1056 | + | |
| 1057 | + | |
| 1058 | + | |
| 1059 | + | |
| 1060 | + | |
| 1061 | + | |
| 1062 | + | |
| 1063 | + | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
| 1068 | + | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
| 1073 | + | |
| 1074 | + | |
| 1075 | + | |
| 1076 | + | |
1053 | 1077 | | |
1054 | 1078 | | |
1055 | 1079 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
114 | | - | |
115 | | - | |
116 | | - | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
117 | 121 | | |
118 | 122 | | |
119 | 123 | | |
120 | | - | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
121 | 127 | | |
122 | 128 | | |
123 | 129 | | |
| |||
404 | 410 | | |
405 | 411 | | |
406 | 412 | | |
407 | | - | |
408 | | - | |
409 | | - | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
410 | 420 | | |
411 | 421 | | |
412 | 422 | | |
413 | | - | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
414 | 426 | | |
415 | 427 | | |
416 | 428 | | |
| |||
0 commit comments