You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(core): enforce input-model strictness and required params in ToolCatalog [TOO-771] (#829)
## Summary
Closes TOO-771 (https://linear.app/arcadedev/issue/TOO-771)
Auto-generated Pydantic input models for `@tool`-decorated functions
currently:
1. Silently drop unknown kwargs (no `extra='forbid'`), so
`edit_requests=...` instead of `requests=...` produces `isError: false`
with zero indication anything was wrong.
2. Default omitted required params to `None` (every field is effectively
optional at validation time, because `create_func_models` passes
`default=None` unconditionally), so the wire schema's `required=True`
never turns into a Pydantic error.
3. Silently drop typos inside nested TypedDict request dicts (`{"txt":
"x"}` instead of `{"text": "x"}`), because the raw TypedDict → Pydantic
path ignores extras.
Reported by francisco@arcade.dev after ~1200 Google Docs edit requests
were discarded across a multi-hour session; the agent had no signal that
anything was being ignored. Production reproductions against
`GoogleDocs.InsertTextAtEndOfDocument@5.2.0` and
`GoogleDocs.EditDocument@5.2.0` confirm the same silent drops at the
deployed Arcade Cloud MCP.
## Changes —
[libs/arcade-core/arcade_core/catalog.py](libs/arcade-core/arcade_core/catalog.py)
- **Top-level strictness**: `create_func_models` now sets
`ConfigDict(extra='forbid')` on the generated input model. Unknown
kwargs raise `ValidationError`, surfaced as `ToolInputError` /
`ErrorKind.TOOL_RUNTIME_BAD_INPUT_VALUE` by the existing
`ToolExecutor._serialize_input` branch.
- **Real required fields**: Required params are emitted without
`default=` so Pydantic enforces them at validation time. Adds a
`ParamInfo.has_explicit_default` flag to distinguish "no default" from
"default=None", preventing regressions on `def f(x: str = None)` style
signatures (which are now correctly typed as `Optional[str]` with
`default=None`).
- **Nested TypedDict strictness (input only)**: New
`create_model_from_typeddict(..., strict=True)` path wraps input-side
TypedDicts in a `_StrictTypedDictBaseModel(extra='forbid')`. Output-side
TypedDicts keep the permissive default so tools returning dicts with
extra keys from upstream APIs (e.g. Google, Slack) don't break.
- **`@model_serializer(mode='wrap')`** on `_TypedDictBaseModel` drops
unset fields during nested serialization. The previous `model_dump()`
Python-level override was bypassed by Pydantic v2's Rust-level outer
serializer.
- **Edge cases handled**: `Optional[TypedDict]`, `list[TypedDict]`,
`list[Optional[TypedDict]]`, `list[TypedDict]` inside a parent
TypedDict, two params sharing a TypedDict type (param-name-qualified
model names to avoid `$defs` collisions), and bare `list` (no type arg)
destructuring.
## Version bumps
`arcade-core` 4.7.0 → 4.8.0 (minor: validation semantics change).
`arcade-tdk` and `arcade-mcp-server` both pin
`arcade-core>=4.8.0,<5.0.0`.
## Review history
This PR went through 3 rounds of local ACR review; round 3 findings
about theoretical `model_serializer` kwarg forwarding (1/5) and
hypothetical non-None TypedDict field defaults (1/5) are documented
in-source and not addressed since the actual call paths are covered by
tests.
## Test plan
- [x] 22 new tests in
[libs/tests/core/test_input_model_strictness.py](libs/tests/core/test_input_model_strictness.py)
covering all three bug shapes + edge cases + regressions.
- [x] Full test suite: 2698 pass, 1 skip, 0 regressions.
- [x] `make check` clean on `arcade-core` (pre-commit, ruff, mypy).
- [x] End-to-end verified: `ToolExecutor.run` now returns
`ErrorKind.TOOL_RUNTIME_BAD_INPUT_VALUE` for both unknown kwargs and
missing required fields (previously both silently succeeded).
- [ ] CI green across Python 3.10–3.14 / Ubuntu / Windows / macOS.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes runtime validation semantics for all tool invocations;
previously accepted bad/missing inputs may now error, which is intended
but can affect agents relying on silent coercion.
>
> **Overview**
> **Tightens auto-generated Pydantic input models** for `@tool`
functions so bad or incomplete arguments fail validation instead of
running tools with silently wrong inputs.
>
> Generated input models now use **`extra='forbid'`**, so misspelled
top-level fields (e.g. `edit_requests` vs `requests`) raise
`ValidationError` and surface as `TOOL_RUNTIME_BAD_INPUT_VALUE` through
`ToolExecutor`.
>
> **Required parameters** are emitted without a Pydantic default (via
new **`has_explicit_default`** on `ParamInfo` / `ToolParamInfo`), fixing
the case where missing required args defaulted to `None`. Signatures
like `x: str = None` still count as optional.
>
> **Input-side TypedDicts** are wrapped through
**`create_model_from_typeddict(..., strict=True)`** and
**`_wrap_typeddicts_as_models`** so nested dict and `list[TypedDict]`
typos are rejected; output TypedDicts stay permissive for extra API
keys. **`@model_serializer`** on `_TypedDictBaseModel` drops unset
`total=False` fields on dump.
>
> Adds **`libs/tests/core/test_input_model_strictness.py`** and bumps
**`arcade-core`** 4.9.0 plus dependent package pins.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
868f2ec. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Eric Gustin <eric@arcade.dev>
0 commit comments