Refine permission control typing#624
Conversation
|
@claude review |
There was a problem hiding this comment.
LGTM — straightforward type annotation refinement resolving the existing TODO.
Extended reasoning...
Overview
This PR touches a single file (src/claude_agent_sdk/types.py) with a 2-line change: it removes a # TODO: Add PermissionUpdate type here comment and replaces list[Any] | None with list[PermissionUpdate] | None on the permission_suggestions field of SDKControlPermissionRequest.
Security risks
None. This is a type annotation change only — no runtime behavior is modified.
Level of scrutiny
Low scrutiny is appropriate. This is a mechanical type-annotation refinement that makes an internal wire-format TypedDict consistent with the downstream ToolPermissionContext.suggestions field, which was already typed as list[PermissionUpdate]. The TODO comment was explicitly requesting this change.
Other factors
The bug report correctly identifies a pre-existing mismatch (PermissionUpdate is a dataclass but the wire data from json.loads() produces plain dicts). However, this issue predates the PR — the downstream consumer was already typed identically. This PR makes the types internally consistent, which is a net improvement even if the broader dataclass-vs-dict gap remains. The nit is worth noting but not worth blocking the PR over.
| input: dict[str, Any] | ||
| # TODO: Add PermissionUpdate type here | ||
| permission_suggestions: list[Any] | None | ||
| permission_suggestions: list[PermissionUpdate] | None |
There was a problem hiding this comment.
🟡 The permission_suggestions field is typed as list[PermissionUpdate], but PermissionUpdate is a @dataclass while this TypedDict represents JSON-parsed wire data from json.loads(), which produces plain dicts. In query.py:276, these raw dicts flow into ToolPermissionContext.suggestions (also typed list[PermissionUpdate]) with no deserialization step, so any user code accessing .type, .rules, or .to_dict() on suggestion items would get AttributeError. Note: the downstream ToolPermissionContext.suggestions type was already list[PermissionUpdate] before this PR, so this is a pre-existing issue — but this change makes it harder to notice by removing the list[Any] / TODO comment that signaled the mismatch.
Extended reasoning...
What the bug is
SDKControlPermissionRequest is a TypedDict that represents JSON-parsed wire data coming from the CLI via json.loads(). The PR changes permission_suggestions from list[Any] (with a TODO comment acknowledging the gap) to list[PermissionUpdate]. However, PermissionUpdate is defined as a @dataclass (around line 107), not a TypedDict. Since json.loads() produces plain Python dicts, the runtime values in permission_suggestions will always be list[dict], never list[PermissionUpdate].
The code path
In query.py:218, raw JSON-parsed data is cast to SDKControlRequest via type: ignore[assignment]. At line 268, the request data is further cast to SDKControlPermissionRequest. Then at line 276:
suggestions=permission_request.get("permission_suggestions", []) or [],These raw dict values are passed directly into ToolPermissionContext.suggestions, which is typed as list[PermissionUpdate]. There is no deserialization step converting dicts to PermissionUpdate dataclass instances anywhere in this path.
Concrete example
If the CLI sends a permission suggestion like {"type": "addRules", "rules": [...], "behavior": "allow"}, it arrives as a plain dict after json.loads(). A user writing a can_use_tool callback who follows the type annotations would write:
async def my_callback(tool_name, input_data, context):
for suggestion in context.suggestions:
if suggestion.type == "addRules": # AttributeError: dict has no attribute type
print(suggestion.rules) # Also AttributeErrorThe correct runtime access would be suggestion["type"] and suggestion["rules"], but the type annotation misleads users into using attribute access.
Why this is pre-existing
The downstream ToolPermissionContext.suggestions field (line 164) was already typed as list[PermissionUpdate] before this PR. The query.py:276 line passing raw dicts into it also predates this PR. The old list[Any] annotation on SDKControlPermissionRequest plus the TODO comment actually served as a signal that this boundary needed attention. This PR removes that signal by making the internal type "consistent" with the already-incorrect downstream type, but the user-facing bug existed before.
Impact
Currently low: the only existing usage (examples/tool_permission_callback.py:37) just logs context.suggestions without accessing individual attributes. However, the misleading type annotation could cause AttributeError for any SDK user who writes a permission callback and trusts the type hints.
How to fix
Either: (a) make PermissionUpdate a TypedDict so json.loads() output matches the type, (b) add a deserialization step in query.py to convert raw dicts to PermissionUpdate instances before passing them to ToolPermissionContext, or (c) revert to list[dict[str, Any]] or list[Any] for the wire-format types and keep the dataclass only for user-constructed values.
What changed
PermissionUpdateandPermissionMode.Why
Any/str.Testing
pytest -q(pass)