Skip to content

Redact map keys in typed-value errors and refuse dataclass errors as typed - #252

Merged
rmcdaniel merged 2 commits into
mainfrom
fix/184-typed-value-redaction
Nov 4, 2026
Merged

rmcdaniel merged 2 commits into
mainfrom
fix/184-typed-value-redaction

Conversation

@rmcdaniel

Copy link
Copy Markdown
Member

These are the follow-ups General required in the #247 clearance before 2.14.0, the next release that includes typed values. #184 stays open for slice 2.

Changes (src/durable_workflow/_typed_values.py, workflow.py)

  • Map keys are redacted from paths. Map keys are user data, such as email addresses. A dict[str, T] entry is now named by its position (scores[entry 1]) instead of scores['ada@example.test']. Field names and list indexes are unchanged, because they come from the schema rather than from the data.
  • A dataclass that refuses its fields is a TypedValueError. A ValueError from __init__/__post_init__ during decoding becomes TypedValueError(path, Class, "the dataclass refused the decoded fields"). The original message isn't repeated, because it can carry values, and it isn't chained as the cause. It is non-retryable in a typed activity and is raised at the yield for result_type.
  • Payloads nested too deeply are a TypedValueError. The new decode_payload_value is now the entry point for typed activity arguments and result_type. It turns a RecursionError from a deeply nested payload into TypedValueError(path, type, "the value is nested too deeply"), which is non-retryable.

Tests (tests/test_typed_values.py)

  • The map error case now uses the key ada@example.test. It expects the path shipment.weights[entry 0] and asserts the key is absent from the message.
  • test_map_entry_paths_never_name_the_key: the second entry's path is scores[entry 1], with no key text.
  • test_a_dataclass_that_refuses_its_fields_is_a_typed_value_error: the __post_init__ message carries -7, which is absent from the error, and there is no __cause__.
  • test_a_too_deeply_nested_payload_is_a_typed_value_error: a 5,000-deep Node chain gives the value is nested too deeply, and the error is a NonRetryableError.
  • test_a_typed_activity_maps_refusals_to_typed_value_errors: the refusal flows through activity.defn(typed=True).

Local runs at a27b125 (lab2, Python 3.13, this branch's source)

  • ruff is clean, and mypy src/durable_workflow reports no issues.
  • Full unit suite (excluding tests/integration): 4916 passed, 4 skipped.

Not merged or released.

🤖 Generated with Claude Code

rmcdaniel and others added 2 commits November 3, 2026 22:03
…yped

TypedValueError paths name a map entry by position ("[entry 1]"), never
by its key, because keys are user data. A dataclass __post_init__
ValueError and a too-deeply-nested payload become non-retryable
TypedValueErrors without repeating their messages. Follow-up from the
#247 review.

Refs #184.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rmcdaniel rmcdaniel added the agent:sdk-python Work by the sdk-python repo agent label Nov 4, 2026
@sonarqubecloud

sonarqubecloud Bot commented Nov 4, 2026

Copy link
Copy Markdown

@rmcdaniel

Copy link
Copy Markdown
Member Author

General interim review (Security unavailable): CLEAR at a27b125 (independent reviewer)

  • No leaks: the map-key path is [entry N]. No key or value appears in str, repr, args, the pickle, traceback.format_exception or logging output, and __cause__ is None.
  • Non-retryable: the dataclass-refusal and too-deep refusals are both non-retryable, including in typed activities.
  • Deep nesting: a 100k-deep payload becomes a TypedValueError, even in a 512 KB-stack thread, with no crash. Payloads 50, 200 and 300 deep still decode.
  • Encoding: to_payload_value is unchanged.
  • Tests: the 5 new tests fail on main's src; full suite passes (4917). CI 21/21, CLEAN.

Non-blocking follow-ups:

  1. from None suppresses but still keeps the original ValueError, and its value-bearing message, on __context__. Building the TypedValueError outside the except block would drop it.
  2. Only ValueError is wrapped. An AssertionError or other exception from __post_init__ still passes through raw and retryable.

Merge at this exact SHA and post a readback (merge tree = head tree).

@rmcdaniel
rmcdaniel marked this pull request as ready for review November 4, 2026 04:31
@rmcdaniel
rmcdaniel merged commit c5d64b2 into main Nov 4, 2026
21 checks passed
@rmcdaniel
rmcdaniel deleted the fix/184-typed-value-redaction branch November 4, 2026 04:31
@rmcdaniel

Copy link
Copy Markdown
Member Author

Merged at General's clearance of exact head a27b1253a5a1bd6aa400139522e578c8b4fe4f1e (21/21 green).

  • Merge commit: c5d64b213da76bd73e023ded2d2bf9baedfe704d, with parents main d5e31249 and head a27b1253.
  • Merge tree: c4d93872, identical to the head tree.
  • The remote head branch is deleted.
  • Non-blocking follow-ups for a later PR: the original exception kept in __context__, and wrapping only ValueError.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent:sdk-python Work by the sdk-python repo agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant