Repository navigation
fix(server): collect stranded defs and memoize the non-compact schema modes - #1513
Conversation
…a claim Follow-up to #1507, which merged before these landed. The 8.x backport's review (#1512) found three things that apply to main too. Repointing a widened union arm strands the definition it left behind, and `_drop_unreachable_defs` runs before the widener so nothing collects it. That is not just dead weight on the payload #1507 exists to shrink: the stranded twin still carries the narrow `status: {"const": "completed"}`, so a client resolving definitions by model name rather than by following `$ref` — which the dotted `$defs` names invite — picks exactly the shape the widening replaces. The widener now reports whether it fired, and unreachable definitions are collected again when it did. It also decides which arm to widen before copying, rather than cloning every arm and discarding all but one: that was shipping three `*LegacyStatus` definitions per tool with one referenced. `schema_mode="inline"` and `"defs"` regenerate the whole Pydantic graph on every call. That is the documented way to restore the pre-#1507 output shape, and it lands on per-request callers, so the remediation was also the expensive path: ~7.5s per call for a 13-tool handler. Generated schemas are now memoized per (kind, mode, tool) and deep-copied out, since a tool definition handed to a caller is an ordinary mutable value. Repeat calls: defs 7.5s -> 0.40s, inline 7.4s -> 1.05s. The anti-smuggling claim in #1507 was too strong. Where a Pydantic model sets `extra="allow"` — `Product` and most response roots — the advertised object is open in every mode, so keeping closed objects closed is schema fidelity, not a filter a buyer can lean on to catch a seller smuggling fields past it. Reworded where it was stated. Tests: the smuggling test now uses `control_media_buy`, whose success arm is both closed and drops an optional, and asserts the clean payload validates first so the unknown key is the only variable — the version in #1507 passed because its sample omitted eight required fields, and passed just as happily with the smuggled key removed. Plus coverage for the non-compact escape hatch actually reaching output schemas (deleting that wiring left 175 tests green), and a sweep for dangling `$ref`s. Refs #1507 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Ladon verdict: Approve
Approve.
Clean follow-up to #1507 applying three #1512 backport findings to main. Reviewer verified: the stranded-def fix re-runs _drop_unreachable_defs after the widener repoints an arm (correct ordering, reachability-safe), the deferred clone yields one *LegacyStatus def per matching arm, and the (kind,mode,name)-keyed schema cache is sound (handler-independent, deep-copied out). fix(server): is the right semver signal — all changes are underscore-prefixed internals with no public adcp.* shape change. Tests back every claim.
Decision-table walk:
- Row 1 (critical/high): no findings — skip.
- Row 2 (gated_paths): false — skip.
- Row 3 (high_risk deleted): only
(modified)— skip. - Row 4/5 (medium findings): none — skip.
- Rows 6–8: no prior escalate, no team gate, zero medium findings — skip.
- Row 9: approve.
High-risk flag is true (src/adcp/server/mcp_tools.py modified) but is a heuristic signal only; reviewer found no medium-or-higher concerns on the modified file, so the modification is presumed safe. review_decision: REVIEW_REQUIRED does not block because gated_paths is false.
Summary
Follow-up to #1507, which merged before these landed. The 8.x backport (#1512) went through its own review, and three findings apply to
maintoo.Stranded
$defssurvive the widening — and the orphan is stricter than the live arm_drop_unreachable_defsruns inside_model_to_json_schema, before_widen_media_buy_output_schema_for_legacy_statusesrepoints a union arm at its widened copy. Nothing collects the definition the arm left behind.That is not just dead weight on the payload #1507 exists to shrink. The stranded twin still carries the narrow
status: {"const": "completed"}, so a client that resolves definitions by model name rather than by following$ref— which the dotted$defsnames invite — picks exactly the shape the widening replaces, and rejects a legitimate negotiated-3.0status: "pending_creatives".The widener now reports whether it fired, and unreachable definitions are collected again when it did. It also decides which arm to widen before copying, instead of cloning every arm and discarding all but one — #1507 shipped three
*LegacyStatusdefinitions per tool with one referenced.The documented escape hatch was also the expensive path
schema_mode="inline"and"defs"regenerate the whole Pydantic graph on every call, with no memoization. That is what #1507 tells a ref-averse adopter to set to restore the previous output shape, and it lands on per-request callers — ~7.5s per call for a 13-tool handler.Schemas are now memoized per
(kind, mode, tool)and deep-copied out, since a tool definition handed to a caller is an ordinary mutable JSON value.defsinlineOne claim in #1507 was too strong
#1507 justified keeping objects closed as "the only check a buyer has against a seller smuggling extra fields into a payload it feeds to a model." Where a Pydantic model sets
extra="allow"—Productand most response roots — the advertised object is open in every mode, so that is not a filter a buyer can lean on. The property actually preserved is fidelity to the runtime model. Reworded everywhere it was stated.Tests
The smuggling test in #1507 was vacuous: its sample omitted eight required fields, so it passed with the smuggled key removed, and
pytest.raisescould not tell the two apart. It now usescontrol_media_buy, whose success arm is both genuinely closed and genuinely drops an optional, and asserts the clean payload validates first so the unknown key is the only variable. Verified to fail under the reverted behaviour.Also added: coverage that the non-compact escape hatch actually reaches output schemas (deleting that wiring left 175 tests green), and a sweep for dangling
$refs across every advertised schema.Reference seller
tools/list: 1,368,877 bytes (down another ~38 KB from the stranded definitions).🤖 Generated with Claude Code
Open workspace in Conductor