mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-28 04:25:08 +02:00
fix(checkpoint): revive lc=2 JSON blobs for safe types without allowlist (#7582)
## Summary Fixes #7498 — `MESSAGE_COERCION_FAILURE` when resuming threads checkpointed before v1.0.1. **Root cause:** PR #6269 (v1.0.1) added an `_allowed_json_modules` security gate to `JsonPlusSerializer._reviver`. The gate defaults to `None`, so old `"json"`-format checkpoint blobs containing `lc=2` constructor dicts (the pre-msgpack serialization format for pydantic objects like `HumanMessage`) are now returned as raw dicts instead of being reconstructed. Those raw dicts reach `add_messages → convert_to_messages`, which sees `type="constructor"` and raises `MESSAGE_COERCION_FAILURE`. Fresh first-turn messages are unaffected because current `dumps_typed` only writes `"msgpack"` blobs. **Fix:** `_reviver` now reconstructs `lc=2` blobs whose target class is already in `SAFE_MSGPACK_TYPES` — the same curated allowlist already used by the msgpack deserialization path (includes all standard LangChain message types). Unknown classes are still blocked, preserving the security intent of #6269. ## Changes - `libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py` — add `_is_safe_json_type()` helper; update `_reviver` and `_check_allowed_json_modules` to allow safe types without an explicit allowlist - `libs/checkpoint/tests/test_jsonplus.py` — two new regression tests: safe-type `lc=2` blobs revive correctly; unknown-type `lc=2` blobs stay blocked ## Test plan - [ ] `test_lc2_json_safe_type_revives_without_allowlist` — `HumanMessage`/`AIMessage` lc=2 JSON blobs round-trip to proper `BaseMessage` objects with no allowlist configured - [ ] `test_lc2_json_unknown_type_stays_blocked_without_allowlist` — `pprint.pprint` lc=2 blob still returns raw dict (not reconstructed) - [ ] `test_deserde_invalid_module` — existing behaviour unchanged - [ ] Full `test_jsonplus.py` suite: 93/93 passing Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
53a9806e65
commit
85cd64ed69
@@ -333,6 +333,57 @@ def test_serde_jsonplus_bytes() -> None:
|
||||
assert serde.loads_typed(dumped) == some_bytes
|
||||
|
||||
|
||||
def test_lc2_json_safe_type_revives_without_allowlist() -> None:
|
||||
"""Old 'json' blobs with lc=2 for safe types must revive without an explicit allowlist.
|
||||
|
||||
Regression test for: https://github.com/langchain-ai/langgraph/issues/7498
|
||||
Threads checkpointed before v1.0.1 (pre-msgpack) stored messages as lc=2 JSON
|
||||
constructor dicts. Resuming those threads must reconstruct proper BaseMessage objects
|
||||
rather than returning raw dicts that cause MESSAGE_COERCION_FAILURE in add_messages.
|
||||
"""
|
||||
from langchain_core.messages import AIMessage
|
||||
|
||||
serde = JsonPlusSerializer() # default: _allowed_json_modules=None
|
||||
|
||||
human_blob = {
|
||||
"lc": 2,
|
||||
"type": "constructor",
|
||||
"id": ["langchain_core", "messages", "human", "HumanMessage"],
|
||||
"kwargs": {"content": "hello", "type": "human"},
|
||||
}
|
||||
ai_blob = {
|
||||
"lc": 2,
|
||||
"type": "constructor",
|
||||
"id": ["langchain_core", "messages", "ai", "AIMessage"],
|
||||
"kwargs": {"content": "hi there", "type": "ai"},
|
||||
}
|
||||
result = serde.loads_typed(("json", json.dumps([human_blob, ai_blob]).encode()))
|
||||
|
||||
assert len(result) == 2
|
||||
assert isinstance(result[0], HumanMessage), (
|
||||
f"Expected HumanMessage, got {type(result[0])}: {result[0]!r}\n"
|
||||
"lc=2 JSON blobs for safe types must deserialize without an explicit allowlist"
|
||||
)
|
||||
assert result[0].content == "hello"
|
||||
assert isinstance(result[1], AIMessage)
|
||||
assert result[1].content == "hi there"
|
||||
|
||||
|
||||
def test_lc2_json_unknown_type_stays_blocked_without_allowlist() -> None:
|
||||
"""lc=2 JSON blobs for types NOT in SAFE_MSGPACK_TYPES still require an allowlist."""
|
||||
serde = JsonPlusSerializer()
|
||||
load = {
|
||||
"lc": 2,
|
||||
"type": "constructor",
|
||||
"id": ["pprint", "pprint"],
|
||||
"kwargs": {"object": "HELLO"},
|
||||
}
|
||||
# No allowlist configured → raw dict returned (not raised, not reconstructed)
|
||||
result = serde.loads_typed(("json", json.dumps(load).encode()))
|
||||
assert isinstance(result, dict), "Unknown lc=2 type must stay as raw dict"
|
||||
assert result.get("lc") == 2
|
||||
|
||||
|
||||
def test_deserde_invalid_module() -> None:
|
||||
serde = JsonPlusSerializer()
|
||||
load = {
|
||||
|
||||
Reference in New Issue
Block a user