Skip to content

fix(pydantic_ai): allowlist kwargs pass-through, drop eager message reshape#610

Merged
Abhijeet Prasad (AbhiPrasad) merged 3 commits into
mainfrom
fix/pydantic-ai-allowlist-drop-eager-reshape
Jul 21, 2026
Merged

fix(pydantic_ai): allowlist kwargs pass-through, drop eager message reshape#610
Abhijeet Prasad (AbhiPrasad) merged 3 commits into
mainfrom
fix/pydantic-ai-allowlist-drop-eager-reshape

Conversation

@starfolkai

@starfolkai starfolkai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

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_metadata and _build_direct_model_input_and_metadata did for 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 flowed usage (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 across Agent.run / run_sync / run_stream / run_stream_events / to_cli_sync and direct.model_request*. Explicitly excludes deps, usage, event_stream_handler, infer_name, instrument.

  • Eager message reshaping dropping fields. _shape_message / _shape_content_part / _shape_model_response reshaped 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 that bt_safe_deep_copy (which already handles dataclasses + Pydantic models) would produce anyway at log time.

    Added _has_binary_leaf walker; 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:

  • Extracted _shape_toolset(ts) from a nested 20-line block inside _build_agent_input_and_metadata.
  • Trimmed docstrings/comments that just restated the helper name. 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).
  • Hoisted import inspect from inside _shape_type to module top.

Test plan

  • No new mocks/fakes. All provider-behavior coverage stays on the existing @pytest.mark.vcr cassette-backed tests.
  • No cassette re-recording. HTTP behavior unchanged; only in-process input/shape logic was touched.
  • 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)" — green
  • nox -s "test_pydantic_ai_logfire(latest)" — 1/1 pass

Two new tests pin the security invariant:

  • test_agent_input_kwargs_are_allowlisted — asserts deps / usage / event_stream_handler / infer_name never land in span input; allowlisted kwargs (instructions, usage_limits, retries, conversation_id, metadata) do.
  • test_direct_model_request_kwargs_are_allowlisted — asserts instrument never leaks; model_settings / model_request_parameters do.

One existing test replaced: test_v2_message_and_response_fields_are_shapedtest_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

…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>
starfolkbot and others added 2 commits July 21, 2026 19:02
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>
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) merged commit fff5bd2 into main Jul 21, 2026
82 checks passed
@AbhiPrasad
Abhijeet Prasad (AbhiPrasad) deleted the fix/pydantic-ai-allowlist-drop-eager-reshape branch July 21, 2026 19:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants