diff --git a/src/claude_agent_sdk/_internal/query.py b/src/claude_agent_sdk/_internal/query.py index affbe181..5d21dd1f 100644 --- a/src/claude_agent_sdk/_internal/query.py +++ b/src/claude_agent_sdk/_internal/query.py @@ -213,13 +213,18 @@ async def _handle_control_request(self, request: SDKControlRequest) -> None: # Convert PermissionResult to expected dict format if isinstance(response, PermissionResultAllow): - response_data = {"allow": True} + response_data = {"behavior": "allow"} if response.updated_input is not None: - response_data["input"] = response.updated_input - # TODO: Handle updatedPermissions when control protocol supports it + response_data["updatedInput"] = response.updated_input + if response.updated_permissions is not None: + response_data["updatedPermissions"] = [ + permission.to_dict() + for permission in response.updated_permissions + ] elif isinstance(response, PermissionResultDeny): - response_data = {"allow": False, "reason": response.message} - # TODO: Handle interrupt flag when control protocol supports it + response_data = {"behavior": "deny", "message": response.message} + if response.interrupt: + response_data["interrupt"] = response.interrupt else: raise TypeError( f"Tool permission callback must return PermissionResult (PermissionResultAllow or PermissionResultDeny), got {type(response)}" diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index 862606e6..4f9c27dd 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -70,6 +70,42 @@ class PermissionUpdate: directories: list[str] | None = None destination: PermissionUpdateDestination | None = None + def to_dict(self) -> dict[str, Any]: + """Convert PermissionUpdate to dictionary format matching TypeScript control protocol.""" + result: dict[str, Any] = { + "type": self.type, + } + + # Add destination for all variants + if self.destination is not None: + result["destination"] = self.destination + + # Handle different type variants + if self.type in ["addRules", "replaceRules", "removeRules"]: + # Rules-based variants require rules and behavior + if self.rules is not None: + result["rules"] = [ + { + "toolName": rule.tool_name, + "ruleContent": rule.rule_content, + } + for rule in self.rules + ] + if self.behavior is not None: + result["behavior"] = self.behavior + + elif self.type == "setMode": + # Mode variant requires mode + if self.mode is not None: + result["mode"] = self.mode + + elif self.type in ["addDirectories", "removeDirectories"]: + # Directory variants require directories + if self.directories is not None: + result["directories"] = self.directories + + return result + # Tool callback types @dataclass diff --git a/tests/test_tool_callbacks.py b/tests/test_tool_callbacks.py index 27634c95..8e69fc5e 100644 --- a/tests/test_tool_callbacks.py +++ b/tests/test_tool_callbacks.py @@ -90,7 +90,7 @@ async def allow_callback( # Check response was sent assert len(transport.written_messages) == 1 response = transport.written_messages[0] - assert '"allow": true' in response + assert '"behavior": "allow"' in response @pytest.mark.asyncio async def test_permission_callback_deny(self): @@ -125,8 +125,8 @@ async def deny_callback( # Check response assert len(transport.written_messages) == 1 response = transport.written_messages[0] - assert '"allow": false' in response - assert '"reason": "Security policy violation"' in response + assert '"behavior": "deny"' in response + assert '"message": "Security policy violation"' in response @pytest.mark.asyncio async def test_permission_callback_input_modification(self): @@ -164,7 +164,7 @@ async def modify_callback( # Check response includes modified input assert len(transport.written_messages) == 1 response = transport.written_messages[0] - assert '"allow": true' in response + assert '"behavior": "allow"' in response assert '"safe_mode": true' in response @pytest.mark.asyncio