Skip to content

Refine permission control typing#624

Open
shuofengzhang wants to merge 2 commits intoanthropics:mainfrom
shuofengzhang:fix/typing-permission-control
Open

Refine permission control typing#624
shuofengzhang wants to merge 2 commits intoanthropics:mainfrom
shuofengzhang:fix/typing-permission-control

Conversation

@shuofengzhang
Copy link
Copy Markdown

What changed

  • Typed control protocol permission fields with PermissionUpdate and PermissionMode.

Why

  • Keeps SDK control request typing aligned with the existing permission models and avoids falling back to Any/str.

Testing

  • pytest -q (pass)

@km-anthropic
Copy link
Copy Markdown
Collaborator

@claude review

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 AttributeError

The 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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants