Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
Attachment,
BlobAttachment,
MCPServerConfig,
PermissionInvocation,
PermissionRequestResult,
PreToolUseHandler,
PreToolUseHookOutput,
Expand Down Expand Up @@ -111,13 +112,20 @@
"""Default timeout in seconds for Copilot requests."""

PermissionHandlerType = Callable[
[PermissionRequest, dict[str, str]], "PermissionRequestResult | Awaitable[PermissionRequestResult]"
[PermissionRequest, dict[str, str] | PermissionInvocation],
"PermissionRequestResult | Awaitable[PermissionRequestResult]",
]
"""Type for permission request handlers. Supports both sync and async callbacks."""

AsyncPermissionHandlerType = Callable[[PermissionRequest, dict[str, str]], "Awaitable[PermissionRequestResult]"]
AsyncPermissionHandlerType = Callable[
[PermissionRequest, dict[str, str] | PermissionInvocation], "Awaitable[PermissionRequestResult]"
]
"""Type for permission request handlers that are always asynchronous."""

_SdkAsyncPermissionHandlerType = Callable[
[PermissionRequest, PermissionInvocation], "Awaitable[PermissionRequestResult]"
]


FunctionApprovalCallback = Callable[[Content], "bool | Awaitable[bool]"]
"""Deprecated approval callback for ``FunctionTool`` instances declared with
Expand Down Expand Up @@ -171,7 +179,7 @@ async def _resolve_function_approval(

def _deny_all_permissions(
_request: PermissionRequest,
_invocation: dict[str, str],
_invocation: PermissionInvocation,
) -> PermissionRequestResult:
"""Default permission handler that denies all requests."""
return PermissionDecisionUserNotAvailable()
Expand Down Expand Up @@ -322,7 +330,7 @@ def _normalize_permission_decision(
return PermissionDecisionApproveForSession(approval=approval)


def _with_normalized_permission_decisions(handler: PermissionHandlerType) -> AsyncPermissionHandlerType:
def _with_normalized_permission_decisions(handler: PermissionHandlerType) -> _SdkAsyncPermissionHandlerType:
"""Wrap a permission handler so its decisions are normalized before reaching the SDK.

Exceptions raised by ``handler`` deliberately propagate: the SDK already catches them
Expand All @@ -335,8 +343,10 @@ def _with_normalized_permission_decisions(handler: PermissionHandlerType) -> Asy
An async handler delegating to ``handler`` and normalizing its result.
"""

async def normalized_handler(request: PermissionRequest, invocation: dict[str, str]) -> PermissionRequestResult:
result = handler(request, invocation)
async def normalized_handler(
request: PermissionRequest, invocation: PermissionInvocation
) -> PermissionRequestResult:
result = handler(request, cast(PermissionInvocation, dict(invocation)))
if inspect.isawaitable(result):
result = await result
return _normalize_permission_decision(result, request)
Expand Down Expand Up @@ -1468,7 +1478,10 @@ def _build_session_kwargs(
if not kwargs.get("model"):
kwargs["model"] = self._settings.get("model") or None
kwargs["on_permission_request"] = _with_normalized_permission_decisions(
opts.get("on_permission_request") or self._permission_handler or _deny_all_permissions
cast(
PermissionHandlerType,
opts.get("on_permission_request") or self._permission_handler or _deny_all_permissions,
)
)
kwargs["hooks"] = self._build_session_hooks(all_tools, kwargs)

Expand Down
2 changes: 1 addition & 1 deletion python/packages/github_copilot/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.15.0,<2",
"github-copilot-sdk==1.0.2; python_version >= '3.11'",
"github-copilot-sdk==1.0.11; python_version >= '3.11'",
]

[tool.uv]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
tool,
)
from agent_framework.exceptions import AgentException
from copilot.session import PermissionHandler, PreToolUseHookInput
from copilot.session import PermissionHandler, PermissionInvocation, PreToolUseHookInput
from copilot.session_events import (
AssistantUsageData,
Data,
Expand Down Expand Up @@ -1329,7 +1329,7 @@ async def test_resume_session_includes_tools_and_permissions(
from copilot.session import PermissionDecisionApproveOnce, PermissionRequestResult
from copilot.session_events import PermissionRequest

def my_handler(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
def my_handler(request: PermissionRequest, context: PermissionInvocation) -> PermissionRequestResult:
return PermissionDecisionApproveOnce()

def my_tool(arg: str) -> str:
Expand Down Expand Up @@ -2603,7 +2603,7 @@ def test_permission_handler_set_when_provided(self) -> None:
from copilot.session import PermissionDecisionApproveOnce, PermissionRequestResult
from copilot.session_events import PermissionRequest

def approve_shell(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
def approve_shell(request: PermissionRequest, context: PermissionInvocation) -> PermissionRequestResult:
if request.kind == "shell":
return PermissionDecisionApproveOnce()
return PermissionDecisionDeniedInteractivelyByUser()
Expand All @@ -2621,7 +2621,7 @@ async def test_session_config_includes_permission_handler(
from copilot.session import PermissionDecisionApproveOnce, PermissionRequestResult
from copilot.session_events import PermissionRequest

def approve_shell_read(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
def approve_shell_read(request: PermissionRequest, context: PermissionInvocation) -> PermissionRequestResult:
if request.kind in ("shell", "read"):
return PermissionDecisionApproveOnce()
return PermissionDecisionDeniedInteractivelyByUser()
Expand Down Expand Up @@ -2943,6 +2943,24 @@ async def async_handler(_request: Any, _invocation: Any) -> Any:
assert isinstance(result, PermissionDecisionApproveForSession)
assert isinstance(result.approval, PermissionDecisionApproveForSessionApprovalCommands)

async def test_legacy_dict_permission_handlers_are_supported(self) -> None:
"""The wrapper continues to support handlers typed for the legacy dictionary context."""
from copilot.generated.rpc import PermissionDecisionApproveOnce
from copilot.session_events import PermissionRequest

received_context: dict[str, str] = {}

def legacy_handler(request: PermissionRequest, context: dict[str, str]) -> Any:
received_context.update(context)
return PermissionDecisionApproveOnce()

from agent_framework_github_copilot._agent import _with_normalized_permission_decisions

handler = _with_normalized_permission_decisions(legacy_handler) # type: ignore[arg-type]
await handler(shell_request(["ls"]), {"session_id": "test-session"})

assert received_context == {"session_id": "test-session"}

async def test_handler_exceptions_propagate(self) -> None:
"""Handler failures must keep reaching the SDK, which denies the request."""
from agent_framework_github_copilot._agent import _with_normalized_permission_decisions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@

from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions
from copilot.generated.rpc import PermissionDecisionDeniedInteractivelyByUser
from copilot.session import PermissionHandler, PermissionRequestResult
from copilot.session import PermissionHandler, PermissionInvocation, PermissionRequestResult
from copilot.session_events import PermissionRequest


async def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
async def prompt_permission(request: PermissionRequest, context: PermissionInvocation) -> PermissionRequestResult:
"""Permission handler that prompts the user for approval."""
print(f"\n[Permission Request: {request.kind}]")
response = (await asyncio.to_thread(input, "Approve? (y/n): ")).strip().lower()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from copilot.generated.rpc import PermissionDecisionReject
from copilot.session import (
PermissionHandler,
PermissionInvocation,
PermissionRequestResult,
PreToolUseHookInput,
PreToolUseHookOutput,
Expand Down Expand Up @@ -63,13 +64,13 @@ def get_weather_detail(location: Annotated[str, "The city and state, e.g. San Fr
)


def approve_all_requests(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
def approve_all_requests(request: PermissionRequest, context: PermissionInvocation) -> PermissionRequestResult:
"""Permission handler that approves every request, including the gated tool."""
print(f"\n [Permission requested: {request.kind}] -> approved")
return PermissionHandler.approve_all(request, context)


def deny_all_requests(request: PermissionRequest, _context: dict[str, str]) -> PermissionRequestResult:
def deny_all_requests(request: PermissionRequest, _context: PermissionInvocation) -> PermissionRequestResult:
"""Permission handler that denies every request."""
print(f"\n [Permission requested: {request.kind}] -> denied")
return PermissionDecisionReject(feedback="Denied by the operator's policy.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
import asyncio

from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions
from copilot.session import PermissionHandler, PermissionRequestResult
from copilot.session import PermissionHandler, PermissionInvocation, PermissionRequestResult
from copilot.session_events import PermissionRequest


def approve_and_log(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
def approve_and_log(request: PermissionRequest, context: PermissionInvocation) -> PermissionRequestResult:
"""Permission handler that auto-approves and logs each permission kind."""
print(f" [Permission: {request.kind}]", flush=True)
return PermissionHandler.approve_all(request, context)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@

from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions
from copilot.generated.rpc import PermissionDecisionUserNotAvailable
from copilot.session import PermissionHandler, PermissionRequestResult
from copilot.session import PermissionHandler, PermissionInvocation, PermissionRequestResult
from copilot.session_events import PermissionRequest


def approve_and_log(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
def approve_and_log(request: PermissionRequest, context: PermissionInvocation) -> PermissionRequestResult:
"""Permission handler that approves only shell commands and logs them."""
if request.kind == "shell":
print(f"\n [Permission: {request.kind}]", flush=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@

from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions
from copilot.generated.rpc import PermissionDecisionUserNotAvailable
from copilot.session import PermissionHandler, PermissionRequestResult
from copilot.session import PermissionHandler, PermissionInvocation, PermissionRequestResult
from copilot.session_events import PermissionRequest


def approve_and_log(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult:
def approve_and_log(request: PermissionRequest, context: PermissionInvocation) -> PermissionRequestResult:
"""Permission handler that approves only URL requests and logs them."""
if request.kind == "url":
print(f"\n [Permission: {request.kind}]", flush=True)
Expand Down
Loading
Loading