update PermissionResult to match latest control protocol (#209)

Recreation of
https://github.com/anthropics/claude-agent-sdk-python/pull/174, with
signed commits
This commit is contained in:
Ashwin Bhat 2025-10-06 14:01:16 -07:00 committed by GitHub
parent 70358589cf
commit 24408f9ddd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 50 additions and 9 deletions

View file

@ -213,13 +213,18 @@ class Query:
# 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)}"

View file

@ -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

View file

@ -90,7 +90,7 @@ class TestToolPermissionCallbacks:
# 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 @@ class TestToolPermissionCallbacks:
# 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 @@ class TestToolPermissionCallbacks:
# 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