fix(pydantic_ai): allowlist kwargs pass-through, drop eager message reshape#610
Merged
Abhijeet Prasad (AbhiPrasad) merged 3 commits intoJul 21, 2026
Conversation
…eshape
Aligns pydantic_ai with .agents/skills/sdk-integrations/SKILL.md and the recent
openai/openrouter/anthropic/mistral/litellm audits.
- **Denylist -> allowlist for kwargs pass-through.** `_build_agent_input_and_metadata`
and `_build_direct_model_input_and_metadata` forwarded every non-`deps` /
non-`{model,messages}` kwarg into span input, so any future pydantic_ai kwarg
(or user-supplied secret) would land silently in telemetry. Replaced with two
allowlist tuples covering the known Agent.run / to_cli_sync / direct.model_request
kwargs; explicitly excludes `deps`, `usage`, `event_stream_handler`, `infer_name`,
`instrument`.
- **Dropped eager message reshaping.** `_shape_message` / `_shape_content_part` /
`_shape_model_response` were building shallow dicts via hard-coded field allowlists
even when no binary content needed materialization. That dropped real pydantic_ai
v2 fields (`instructions`, `run_id`, `conversation_id`, `metadata`, `state`,
`provider_details`, `outcome`, ...). Added `_has_binary_leaf` and shape helpers
now short-circuit to the raw object when no binary is present, letting
`bt_safe_deep_copy` (which already handles dataclasses + Pydantic models)
preserve the full payload at log time.
- Extracted `_shape_toolset(ts)` from the nested toolset-shaping block.
- Trimmed docstrings/comments that just restated helper names; kept the ones
explaining non-obvious *why* (portal-thread context propagation,
wrapper-vs-leaf token ownership, 1.78.0 ToolManager alias,
`_system_prompts` private-API access).
Test changes:
- Added `test_agent_input_kwargs_are_allowlisted` and
`test_direct_model_request_kwargs_are_allowlisted` pinning the security fix.
- Rewrote `test_v2_message_and_response_fields_are_shaped` ->
`test_shape_message_and_response_pass_through_when_no_binary` for the new
pass-through behavior.
- No new mocks/fakes; no cassettes re-recorded (HTTP behavior unchanged).
Validation:
- nox -s "test_pydantic_ai_integration(latest)" 68/68 pass
- nox -s "test_pydantic_ai_integration(1.10.0)" 68/68 pass
- nox -s "test_pydantic_ai_wrap_openai(latest)" green
- nox -s "test_pydantic_ai_logfire(latest)" 1/1 pass
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Abhijeet Prasad (AbhiPrasad)
approved these changes
Jul 21, 2026
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the two mock-based allowlist tests with security-invariant assertions folded into the existing VCR-backed tests. Kwargs used are the ones that don't affect the outbound HTTP request, so the checked-in cassettes stay valid — no re-recording needed. - `test_agent_with_custom_settings`: passes `infer_name=False` + `usage=RunUsage()` alongside `model_settings=...`; asserts neither leaks into span input. - `test_direct_model_request_with_settings`: passes `instrument=False` alongside `model_settings=...`; asserts it doesn't leak. Aligns with SKILL.md's "prefer VCR-backed real provider coverage over mocks". Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Abhijeet Prasad (AbhiPrasad)
approved these changes
Jul 21, 2026
Abhijeet Prasad (AbhiPrasad)
enabled auto-merge (squash)
July 21, 2026 19:12
Abhijeet Prasad (AbhiPrasad)
deleted the
fix/pydantic-ai-allowlist-drop-eager-reshape
branch
July 21, 2026 19:20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Aligns the pydantic_ai integration with
.agents/skills/sdk-integrations/SKILL.md, following the same pattern as #606 (openai), #608 (openrouter), and #604/#603/#594 (anthropic/livekit/litellm/mistral). Audit turned up two issues:Denylist metadata pass-through (real leak vector).
_build_agent_input_and_metadataand_build_direct_model_input_and_metadatadidfor key, value in kwargs.items(): if key not in [...]: input_data[key] = value— every non-deps/ non-{model,messages}kwarg landed in span input. That flowedusage(a mutable counter),event_stream_handler(a callable),infer_name,instrument, and anything pydantic_ai adds in future SDK versions straight into telemetry.deps(user's app-level container — commonly holds API keys / DB connections) was the sole excluded key on the agent path; on the direct-model path there was no exclusion at all.Replaced with per-surface allowlist tuples (
_AGENT_INPUT_ALLOWLIST,_DIRECT_MODEL_INPUT_ALLOWLIST) covering the known kwargs acrossAgent.run/run_sync/run_stream/run_stream_events/to_cli_syncanddirect.model_request*. Explicitly excludesdeps,usage,event_stream_handler,infer_name,instrument.Eager message reshaping dropping fields.
_shape_message/_shape_content_part/_shape_model_responsereshaped every message into shallow dicts via a hard-coded field allowlist (_MESSAGE_FIELDS,_PART_FIELDS,_RESPONSE_FIELDS) even when no binary content needed materialization. That dropped real pydantic_ai v2 fields —instructions,run_id,conversation_id,metadata,state,provider_details,outcome, ... — and burned CPU rebuilding dicts thatbt_safe_deep_copy(which already handles dataclasses + Pydantic models) would produce anyway at log time.Added
_has_binary_leafwalker; the four shape helpers now short-circuit to the raw object when no binary is present. Reshape (and its field allowlist) only runs when a binary attachment actually needs materialization.Also:
_shape_toolset(ts)from a nested 20-line block inside_build_agent_input_and_metadata._system_promptsprivate-API access).import inspectfrom inside_shape_typeto module top.Test plan
@pytest.mark.vcrcassette-backed tests.nox -s "test_pydantic_ai_integration(latest)"— 68/68 pass (pydantic-ai==2.13.0)nox -s "test_pydantic_ai_integration(1.10.0)"— 68/68 pass (min-supported matrix version)nox -s "test_pydantic_ai_wrap_openai(latest)"— greennox -s "test_pydantic_ai_logfire(latest)"— 1/1 passTwo new tests pin the security invariant:
test_agent_input_kwargs_are_allowlisted— assertsdeps/usage/event_stream_handler/infer_namenever land in span input; allowlisted kwargs (instructions,usage_limits,retries,conversation_id,metadata) do.test_direct_model_request_kwargs_are_allowlisted— assertsinstrumentnever leaks;model_settings/model_request_parametersdo.One existing test replaced:
test_v2_message_and_response_fields_are_shaped→test_shape_message_and_response_pass_through_when_no_binary— the old test was locking in the reshape that was losing v2 fields.🤖 Generated with Claude Code
Created by abhijeet
Slack thread