diff --git a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py index 0fc9c9dcf6..51b5791372 100644 --- a/python/packages/github_copilot/agent_framework_github_copilot/_agent.py +++ b/python/packages/github_copilot/agent_framework_github_copilot/_agent.py @@ -37,9 +37,10 @@ from agent_framework.exceptions import AgentException from agent_framework.observability import AgentTelemetryLayer try: - from copilot import CopilotClient, CopilotSession, SubprocessConfig - from copilot.generated.session_events import PermissionRequest, SessionEvent, SessionEventType + from copilot import CopilotClient, CopilotSession, RuntimeConnection + from copilot.generated.rpc import PermissionDecisionUserNotAvailable from copilot.session import MCPServerConfig, PermissionRequestResult, ProviderConfig, SystemMessageConfig + from copilot.session_events import PermissionRequest, SessionEvent, SessionEventType from copilot.tools import Tool as CopilotTool from copilot.tools import ToolInvocation, ToolResult except ImportError as _copilot_import_error: @@ -57,8 +58,10 @@ else: DEFAULT_TIMEOUT_SECONDS: float = 60.0 """Default timeout in seconds for Copilot requests.""" -PermissionHandlerType = Callable[[PermissionRequest, dict[str, str]], PermissionRequestResult] -"""Type for permission request handlers.""" +PermissionHandlerType = Callable[ + [PermissionRequest, dict[str, str]], "PermissionRequestResult | Awaitable[PermissionRequestResult]" +] +"""Type for permission request handlers. Supports both sync and async callbacks.""" FunctionApprovalCallback = Callable[[Content], "bool | Awaitable[bool]"] @@ -121,7 +124,7 @@ def _deny_all_permissions( _invocation: dict[str, str], ) -> PermissionRequestResult: """Default permission handler that denies all requests.""" - return PermissionRequestResult() + return PermissionDecisionUserNotAvailable() class GitHubCopilotSettings(TypedDict, total=False): @@ -140,9 +143,9 @@ class GitHubCopilotSettings(TypedDict, total=False): Can be set via environment variable GITHUB_COPILOT_TIMEOUT. log_level: CLI log level. Can be set via environment variable GITHUB_COPILOT_LOG_LEVEL. - copilot_home: Directory where the CLI stores session state, configuration, + base_directory: Directory where the CLI stores session state, configuration, and other persistent data. Can be set via environment variable - GITHUB_COPILOT_COPILOT_HOME. Defaults to ~/.copilot when not set. + GITHUB_COPILOT_BASE_DIRECTORY. Defaults to ~/.copilot when not set. Only applicable when the SDK spawns the CLI process (ignored when connecting to an external server via a pre-configured client). """ @@ -151,7 +154,7 @@ class GitHubCopilotSettings(TypedDict, total=False): model: str | None timeout: float | None log_level: str | None - copilot_home: str | None + base_directory: str | None class GitHubCopilotOptions(TypedDict, total=False): @@ -314,7 +317,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]): provider: ProviderConfig | None = opts.pop("provider", None) instruction_directories: list[str] | None = opts.pop("instruction_directories", None) on_function_approval: FunctionApprovalCallback | None = opts.pop("on_function_approval", None) - copilot_home = opts.pop("copilot_home", None) + base_directory = opts.pop("base_directory", None) self._settings = load_settings( GitHubCopilotSettings, @@ -323,7 +326,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]): model=model, timeout=timeout, log_level=log_level, - copilot_home=copilot_home, + base_directory=base_directory, env_file_path=env_file_path, env_file_encoding=env_file_encoding, ) @@ -362,14 +365,16 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]): if self._client is None: cli_path = self._settings.get("cli_path") or None log_level = self._settings.get("log_level") or None - copilot_home = self._settings.get("copilot_home") or None + base_directory = self._settings.get("base_directory") or None - subprocess_kwargs: dict[str, Any] = {"cli_path": cli_path} + client_kwargs: dict[str, Any] = {} + if cli_path: + client_kwargs["connection"] = RuntimeConnection.for_stdio(path=cli_path) if log_level: - subprocess_kwargs["log_level"] = log_level - if copilot_home: - subprocess_kwargs["copilot_home"] = copilot_home - self._client = CopilotClient(SubprocessConfig(**subprocess_kwargs)) + client_kwargs["log_level"] = log_level + if base_directory: + client_kwargs["base_directory"] = base_directory + self._client = CopilotClient(**client_kwargs) try: await self._client.start() diff --git a/python/packages/github_copilot/pyproject.toml b/python/packages/github_copilot/pyproject.toml index 875e999804..2256c4255f 100644 --- a/python/packages/github_copilot/pyproject.toml +++ b/python/packages/github_copilot/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dependencies = [ "agent-framework-core>=1.6.0,<2", - "github-copilot-sdk>=1.0.0b2,<=1.0.0b2; python_version >= '3.11'", + "github-copilot-sdk>=1.0.0,<2; python_version >= '3.11'", ] [tool.uv] diff --git a/python/packages/github_copilot/tests/test_github_copilot_agent.py b/python/packages/github_copilot/tests/test_github_copilot_agent.py index a0f0caef72..518d575820 100644 --- a/python/packages/github_copilot/tests/test_github_copilot_agent.py +++ b/python/packages/github_copilot/tests/test_github_copilot_agent.py @@ -2,6 +2,7 @@ # ruff: noqa: E402 +import os import unittest.mock from datetime import datetime, timezone from typing import Any @@ -20,9 +21,11 @@ from agent_framework import ( ContextProvider, HistoryProvider, Message, + tool, ) from agent_framework.exceptions import AgentException -from copilot.generated.session_events import ( +from copilot.session import PermissionHandler +from copilot.session_events import ( Data, SessionEvent, SessionEventType, @@ -308,27 +311,27 @@ class TestGitHubCopilotAgentLifecycle: ) await agent.start() - call_args = MockClient.call_args[0][0] - assert call_args.cli_path == "/custom/path" - assert call_args.log_level == "debug" + kwargs = MockClient.call_args.kwargs + assert kwargs["connection"].path == "/custom/path" + assert kwargs["log_level"] == "debug" - async def test_start_passes_copilot_home_to_subprocess_config(self) -> None: - """Test that copilot_home is passed through to SubprocessConfig.""" + async def test_start_passes_base_directory_to_client(self) -> None: + """Test that base_directory is passed through to CopilotClient.""" with patch("agent_framework_github_copilot._agent.CopilotClient") as MockClient: mock_client = MagicMock() mock_client.start = AsyncMock() MockClient.return_value = mock_client agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( - default_options={"copilot_home": "/custom/copilot/home"} + default_options={"base_directory": "/custom/copilot/home"} ) await agent.start() - call_args = MockClient.call_args[0][0] - assert call_args.copilot_home == "/custom/copilot/home" + kwargs = MockClient.call_args.kwargs + assert kwargs["base_directory"] == "/custom/copilot/home" - async def test_start_copilot_home_not_set_when_unspecified(self) -> None: - """Test that copilot_home is not included in SubprocessConfig when not specified.""" + async def test_start_base_directory_not_set_when_unspecified(self) -> None: + """Test that base_directory is not included in client kwargs when not specified.""" with patch("agent_framework_github_copilot._agent.CopilotClient") as MockClient: mock_client = MagicMock() mock_client.start = AsyncMock() @@ -337,14 +340,14 @@ class TestGitHubCopilotAgentLifecycle: agent = GitHubCopilotAgent() await agent.start() - call_args = MockClient.call_args[0][0] - assert call_args.copilot_home is None + kwargs = MockClient.call_args.kwargs + assert "base_directory" not in kwargs - async def test_start_copilot_home_from_env_variable(self) -> None: - """Test that copilot_home can be set via GITHUB_COPILOT_COPILOT_HOME env variable.""" + async def test_start_base_directory_from_env_variable(self) -> None: + """Test that base_directory can be set via GITHUB_COPILOT_BASE_DIRECTORY env variable.""" with ( patch("agent_framework_github_copilot._agent.CopilotClient") as MockClient, - patch.dict("os.environ", {"GITHUB_COPILOT_COPILOT_HOME": "/env/copilot/home"}), + patch.dict("os.environ", {"GITHUB_COPILOT_BASE_DIRECTORY": "/env/copilot/home"}), ): mock_client = MagicMock() mock_client.start = AsyncMock() @@ -353,8 +356,8 @@ class TestGitHubCopilotAgentLifecycle: agent = GitHubCopilotAgent() await agent.start() - call_args = MockClient.call_args[0][0] - assert call_args.copilot_home == "/env/copilot/home" + kwargs = MockClient.call_args.kwargs + assert kwargs["base_directory"] == "/env/copilot/home" class TestGitHubCopilotAgentRun: @@ -1053,11 +1056,11 @@ class TestGitHubCopilotAgentSessionManagement: mock_session: MagicMock, ) -> None: """Test that resumed session config includes tools and permission handler.""" - from copilot.generated.session_events import PermissionRequest - from copilot.session import PermissionRequestResult + from copilot.session import PermissionDecisionApproveOnce, PermissionRequestResult + from copilot.session_events import PermissionRequest def my_handler(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - return PermissionRequestResult(kind="approved") + return PermissionDecisionApproveOnce() def my_tool(arg: str) -> str: """A test tool.""" @@ -1869,6 +1872,15 @@ class TestGitHubCopilotAgentErrorHandling: class TestGitHubCopilotAgentPermissions: """Test cases for permission handling.""" + def test_deny_all_permissions_returns_user_not_available(self) -> None: + """Test that the default deny handler returns PermissionDecisionUserNotAvailable.""" + from copilot.generated.rpc import PermissionDecisionUserNotAvailable + + from agent_framework_github_copilot._agent import _deny_all_permissions + + result = _deny_all_permissions(MagicMock(), {}) + assert isinstance(result, PermissionDecisionUserNotAvailable) + def test_no_permission_handler_when_not_provided(self) -> None: """Test that no handler is set when on_permission_request is not provided.""" agent = GitHubCopilotAgent() @@ -1876,13 +1888,14 @@ class TestGitHubCopilotAgentPermissions: def test_permission_handler_set_when_provided(self) -> None: """Test that a handler is set when on_permission_request is provided.""" - from copilot.generated.session_events import PermissionRequest - from copilot.session import PermissionRequestResult + from copilot.generated.rpc import PermissionDecisionDeniedInteractivelyByUser + from copilot.session import PermissionDecisionApproveOnce, PermissionRequestResult + from copilot.session_events import PermissionRequest def approve_shell(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: if request.kind == "shell": - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") + return PermissionDecisionApproveOnce() + return PermissionDecisionDeniedInteractivelyByUser() agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( default_options={"on_permission_request": approve_shell} @@ -1895,13 +1908,14 @@ class TestGitHubCopilotAgentPermissions: mock_session: MagicMock, ) -> None: """Test that session config includes permission handler when provided.""" - from copilot.generated.session_events import PermissionRequest - from copilot.session import PermissionRequestResult + from copilot.generated.rpc import PermissionDecisionDeniedInteractivelyByUser + from copilot.session import PermissionDecisionApproveOnce, PermissionRequestResult + from copilot.session_events import PermissionRequest def approve_shell_read(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: if request.kind in ("shell", "read"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") + return PermissionDecisionApproveOnce() + return PermissionDecisionDeniedInteractivelyByUser() agent: GitHubCopilotAgent[GitHubCopilotOptions] = GitHubCopilotAgent( client=mock_client, @@ -2705,3 +2719,163 @@ class TestGitHubCopilotAgentContextProviders: assert call_kwargs.get("tools") is not None tool_names = [t.name for t in call_kwargs["tools"]] assert "load_skill" in tool_names + + +# --------------------------------------------------------------------------- +# Integration tests — require COPILOT_GITHUB_TOKEN env var +# --------------------------------------------------------------------------- + +skip_if_copilot_integration_tests_disabled = pytest.mark.skipif( + os.getenv("COPILOT_GITHUB_TOKEN", "") == "", + reason="No COPILOT_GITHUB_TOKEN provided; skipping integration tests.", +) + + +@tool(approval_mode="never_require") +def get_weather(location: str) -> str: + """Get the weather for a given location.""" + return f"The weather in {location} is sunny with a high of 25C." + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_with_simple_prompt_returns_response() -> None: + """Integration test: basic non-streaming response.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant. Keep your answers short.", + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session = agent.create_session() + response = await agent.run("What is 2 + 2? Answer with just the number.", session=session) + + assert response is not None + assert len(response.messages) > 0 + assert "4" in response.text + + if session.service_session_id and agent._client: + await agent._client.delete_session(session.service_session_id) + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_streaming_returns_updates() -> None: + """Integration test: streaming response yields updates.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant. Keep your answers short.", + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session = agent.create_session() + updates = [] + async for chunk in agent.run("Count from 1 to 5.", stream=True, session=session): + updates.append(chunk) + + assert len(updates) > 0 + full_text = "".join(u.text for u in updates if u.text) + assert len(full_text) > 0 + + if session.service_session_id and agent._client: + await agent._client.delete_session(session.service_session_id) + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_with_function_tool_invokes_tool() -> None: + """Integration test: function tool is invoked by the agent.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful weather agent. Use the get_weather tool to answer weather questions.", + tools=[get_weather], + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session = agent.create_session() + response = await agent.run("What's the weather like in Seattle?", session=session) + + assert response is not None + assert len(response.messages) > 0 + assert any(word in response.text.lower() for word in ["sunny", "25", "weather", "seattle"]) + + if session.service_session_id and agent._client: + await agent._client.delete_session(session.service_session_id) + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_with_session_maintains_context() -> None: + """Integration test: session maintains conversation context across turns.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant. Keep your answers short.", + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session = agent.create_session() + + response1 = await agent.run("My name is Alice.", session=session) + assert response1 is not None + + response2 = await agent.run("What is my name?", session=session) + + assert response2 is not None + assert "alice" in response2.text.lower() + + if session.service_session_id and agent._client: + await agent._client.delete_session(session.service_session_id) + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_with_session_resume_continues_conversation() -> None: + """Integration test: session can be resumed by ID.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant. Keep your answers short.", + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session1 = agent.create_session() + await agent.run("Remember this number: 42.", session=session1) + + session_id = session1.service_session_id + assert session_id is not None + + session2 = AgentSession() + session2.service_session_id = session_id + + response = await agent.run("What number did I ask you to remember?", session=session2) + + assert response is not None + assert "42" in response.text + + if agent._client: + await agent._client.delete_session(session_id) + + +@pytest.mark.flaky +@pytest.mark.integration +@skip_if_copilot_integration_tests_disabled +async def test_integration_run_with_shell_permissions_executes_command() -> None: + """Integration test: shell commands can be executed with permission handler.""" + agent = GitHubCopilotAgent( + instructions="You are a helpful assistant that can execute shell commands.", + default_options={"on_permission_request": PermissionHandler.approve_all}, + ) + + async with agent: + session = agent.create_session() + response = await agent.run("Run a shell command to print 'hello world'", session=session) + + assert response is not None + assert "hello" in response.text.lower() + + if session.service_session_id and agent._client: + await agent._client.delete_session(session.service_session_id) diff --git a/python/samples/02-agents/providers/github_copilot/README.md b/python/samples/02-agents/providers/github_copilot/README.md index c3132ed1e9..d58698e7da 100644 --- a/python/samples/02-agents/providers/github_copilot/README.md +++ b/python/samples/02-agents/providers/github_copilot/README.md @@ -23,7 +23,7 @@ The following environment variables can be configured: | `GITHUB_COPILOT_MODEL` | Model to use (e.g., "gpt-5", "claude-sonnet-4") | Server default | | `GITHUB_COPILOT_TIMEOUT` | Request timeout in seconds | `60` | | `GITHUB_COPILOT_LOG_LEVEL` | CLI log level | `info` | -| `GITHUB_COPILOT_COPILOT_HOME` | Directory for CLI session state and config | `~/.copilot` | +| `GITHUB_COPILOT_BASE_DIRECTORY` | Directory for CLI session state and config | `~/.copilot` | ## Observability diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_basic.py b/python/samples/02-agents/providers/github_copilot/github_copilot_basic.py index 93e37d89bb..3cbfe01795 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_basic.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_basic.py @@ -19,8 +19,7 @@ from typing import Annotated from agent_framework import tool from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.session import PermissionHandler from dotenv import load_dotenv from pydantic import Field @@ -28,19 +27,6 @@ from pydantic import Field load_dotenv() -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - if request.full_command_text is not None: - print(f" Command: {request.full_command_text}") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") - - # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; # see samples/02-agents/tools/function_tool_with_approval.py # and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. @@ -60,7 +46,7 @@ async def non_streaming_example() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: @@ -77,7 +63,7 @@ async def streaming_example() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: @@ -97,7 +83,7 @@ async def runtime_options_example() -> None: agent = GitHubCopilotAgent( instructions="Always respond in exactly 3 words.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py index 1a82d9867d..67336259d0 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_file_operations.py @@ -4,8 +4,7 @@ GitHub Copilot Agent with File Operation Permissions This sample demonstrates how to enable file read and write operations with GitHubCopilotAgent. -By providing a permission handler that approves "read" and/or "write" requests, the agent can -read from and write to files on the filesystem. +By providing a permission handler, the agent can read from and write to files on the filesystem. SECURITY NOTE: Only enable file permissions when you trust the agent's actions. - "read" allows the agent to read any accessible file @@ -15,21 +14,18 @@ SECURITY NOTE: Only enable file permissions when you trust the agent's actions. import asyncio from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionDeniedInteractivelyByUser +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session_events import PermissionRequest -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: +async def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: """Permission handler that prompts the user for approval.""" print(f"\n[Permission Request: {request.kind}]") - - if request.path is not None: - print(f" Path: {request.path}") - - response = input("Approve? (y/n): ").strip().lower() + response = (await asyncio.to_thread(input, "Approve? (y/n): ")).strip().lower() if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") + return PermissionHandler.approve_all(request, context) + return PermissionDecisionDeniedInteractivelyByUser() async def main() -> None: diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_function_approval.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_function_approval.py index 17520fdf91..3348dbf1a5 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_function_approval.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_function_approval.py @@ -32,6 +32,7 @@ from typing import Annotated from agent_framework import Content, tool from agent_framework.github import GitHubCopilotAgent +from copilot.session import PermissionHandler from dotenv import load_dotenv load_dotenv() @@ -48,37 +49,42 @@ def get_weather_detail(location: Annotated[str, "The city and state, e.g. San Fr ) -def prompt_for_approval(call: Content) -> bool: - """Synchronous approval prompt. +async def prompt_for_approval(call: Content) -> bool: + """Async approval callback that prompts the user interactively. The callback receives a ``FunctionCallContent`` so the operator can review the tool name and arguments before deciding. Returning ``True`` allows the call; returning ``False`` denies it and a tool-error is returned to the model. + + Uses ``asyncio.to_thread`` so the event loop is not blocked by ``input()``. """ - print(f"\n[Function Approval Request]\n Tool: {call.name}\n Arguments: {call.arguments}") - response = input("Approve this tool call? (y/n): ").strip().lower() + print(f"\n [Function Approval Request]\n Tool: {call.name}\n Arguments: {call.arguments}") + response = (await asyncio.to_thread(input, " Approve this tool call? (y/n): ")).strip().lower() return response in ("y", "yes") -async def prompt_for_approval_async(call: Content) -> bool: - """Async approval prompt. +def auto_approve(call: Content) -> bool: + """Synchronous approval callback that always approves. - Use an async callback when approval requires I/O (e.g. an HTTP call to a - review service or queueing the request to a UI). ``input()`` is wrapped - with ``asyncio.to_thread`` so the event loop is not blocked. + Use a sync callback for simple, non-blocking decisions that don't require + I/O (e.g. checking an allow-list of tool names). """ - print(f"\n[Function Approval Request - async]\n Tool: {call.name}\n Arguments: {call.arguments}") - response = await asyncio.to_thread(input, "Approve this tool call? (y/n): ") - return response.strip().lower() in ("y", "yes") + print(f"\n [Function Approval Request]\n Tool: {call.name}\n Arguments: {call.arguments}") + print(" -> Auto-approved") + return True -async def run_with_sync_callback() -> None: - print("\n=== GitHub Copilot Agent: synchronous approval callback ===") +async def run_with_interactive_callback() -> None: + """Demonstrates an interactive approval prompt before tool execution.""" + print("\n=== GitHub Copilot Agent: interactive approval callback ===") agent = GitHubCopilotAgent( instructions="You are a helpful weather assistant.", tools=[get_weather_detail], - default_options={"on_function_approval": prompt_for_approval}, + default_options={ + "on_function_approval": prompt_for_approval, + "on_permission_request": PermissionHandler.approve_all, + }, ) async with agent: query = "Give me the detailed weather for Seattle." @@ -87,12 +93,16 @@ async def run_with_sync_callback() -> None: print(f"Agent: {result}") -async def run_with_async_callback() -> None: - print("\n=== GitHub Copilot Agent: asynchronous approval callback ===") +async def run_with_auto_approve_callback() -> None: + """Demonstrates a synchronous callback that always approves.""" + print("\n=== GitHub Copilot Agent: synchronous auto-approve callback ===") agent = GitHubCopilotAgent( instructions="You are a helpful weather assistant.", tools=[get_weather_detail], - default_options={"on_function_approval": prompt_for_approval_async}, + default_options={ + "on_function_approval": auto_approve, + "on_permission_request": PermissionHandler.approve_all, + }, ) async with agent: query = "Give me the detailed weather for Tokyo." @@ -112,6 +122,7 @@ async def run_without_callback() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful weather assistant.", tools=[get_weather_detail], + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: query = "Give me the detailed weather for Paris." @@ -122,8 +133,8 @@ async def run_without_callback() -> None: async def main() -> None: print("=== GitHub Copilot Agent: Function approval enforcement ===") - await run_with_sync_callback() - await run_with_async_callback() + await run_with_interactive_callback() + await run_with_auto_approve_callback() await run_without_callback() diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py index e30f114f7d..4c7ae2c1a4 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_instruction_directories.py @@ -22,24 +22,13 @@ import asyncio from pathlib import Path from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.session import PermissionHandler from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") - - async def default_instructions_example() -> None: """Example of pointing the agent at project-specific instruction directories.""" print("=== Instruction Directories (Default) ===\n") @@ -58,7 +47,7 @@ async def default_instructions_example() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful coding assistant.", default_options={ - "on_permission_request": prompt_permission, + "on_permission_request": PermissionHandler.approve_all, "instruction_directories": instruction_dirs, }, ) @@ -79,7 +68,7 @@ async def runtime_override_example() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful assistant.", default_options={ - "on_permission_request": prompt_permission, + "on_permission_request": PermissionHandler.approve_all, "instruction_directories": ["/team/shared/instructions"], }, ) diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_mcp.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_mcp.py index 71bd67efb4..60e4704c07 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_mcp.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_mcp.py @@ -15,24 +15,13 @@ of MCP-related actions. import asyncio from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import MCPServerConfig, PermissionRequestResult +from copilot.session import MCPServerConfig, PermissionHandler from dotenv import load_dotenv # Load environment variables from .env file load_dotenv() -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") - - async def main() -> None: print("=== GitHub Copilot Agent with MCP Servers ===\n") @@ -56,7 +45,7 @@ async def main() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful assistant with access to the local filesystem and Microsoft Learn.", default_options={ - "on_permission_request": prompt_permission, + "on_permission_request": PermissionHandler.approve_all, "mcp_servers": mcp_servers, }, ) diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_multiple_permissions.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_multiple_permissions.py index 916061f939..5da43b3274 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_multiple_permissions.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_multiple_permissions.py @@ -3,9 +3,8 @@ """ GitHub Copilot Agent with Multiple Permissions -This sample demonstrates how to enable multiple permission types with GitHubCopilotAgent. -By combining different permission kinds in the handler, the agent can perform complex tasks -that require multiple capabilities. +This sample demonstrates how multiple permission types are requested when GitHubCopilotAgent +performs complex tasks that require different capabilities. Available permission kinds: - "shell": Execute shell commands @@ -21,23 +20,14 @@ More permissions mean more potential for unintended actions. import asyncio from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session_events import PermissionRequest -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - if request.full_command_text is not None: - print(f" Command: {request.full_command_text}") - if request.path is not None: - print(f" Path: {request.path}") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") +def approve_and_log(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: + """Permission handler that auto-approves and logs each permission kind.""" + print(f" [Permission: {request.kind}]", flush=True) + return PermissionHandler.approve_all(request, context) async def main() -> None: @@ -45,14 +35,14 @@ async def main() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful development assistant that can read, write files and run commands.", - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": approve_and_log}, ) async with agent: query = "List the first 3 Python files, then read the first one and create a summary in summary.txt" - print(f"User: {query}") + print(f"User: {query}\n") result = await agent.run(query) - print(f"Agent: {result}\n") + print(f"\nAgent: {result}\n") if __name__ == "__main__": diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py index 801edf52f3..70a0f3c826 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_session.py @@ -14,24 +14,10 @@ from typing import Annotated from agent_framework import tool from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.session import PermissionHandler from pydantic import Field -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - if request.full_command_text is not None: - print(f" Command: {request.full_command_text}") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") - - # NOTE: approval_mode="never_require" is for sample brevity. Use "always_require" in production; # see samples/02-agents/tools/function_tool_with_approval.py # and samples/02-agents/tools/function_tool_with_approval_and_sessions.py. @@ -51,7 +37,7 @@ async def example_with_automatic_session_creation() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: @@ -76,7 +62,7 @@ async def example_with_session_persistence() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent: @@ -113,7 +99,7 @@ async def example_with_existing_session_id() -> None: agent1 = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent1: @@ -135,7 +121,7 @@ async def example_with_existing_session_id() -> None: agent2 = GitHubCopilotAgent( instructions="You are a helpful weather agent.", tools=[get_weather], - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": PermissionHandler.approve_all}, ) async with agent2: diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_shell.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_shell.py index 729aad6863..66ead3bf99 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_shell.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_shell.py @@ -14,21 +14,20 @@ Shell commands have full access to your system within the permissions of the run import asyncio from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionUserNotAvailable +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session_events import PermissionRequest -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - if request.full_command_text is not None: - print(f" Command: {request.full_command_text}") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") +def approve_and_log(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: + """Permission handler that approves only shell commands and logs them.""" + if request.kind == "shell": + print(f"\n [Permission: {request.kind}]", flush=True) + command = getattr(request, "full_command_text", None) + if command is not None: + print(f" Command: {command}", flush=True) + return PermissionHandler.approve_all(request, context) + return PermissionDecisionUserNotAvailable() async def main() -> None: @@ -36,14 +35,14 @@ async def main() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful assistant that can execute shell commands.", - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": approve_and_log}, ) async with agent: query = "List the first 3 Python files in the current directory" print(f"User: {query}") result = await agent.run(query) - print(f"Agent: {result}\n") + print(f"\nAgent: {result}\n") if __name__ == "__main__": diff --git a/python/samples/02-agents/providers/github_copilot/github_copilot_with_url.py b/python/samples/02-agents/providers/github_copilot/github_copilot_with_url.py index 2f14648bae..61fa90cd5d 100644 --- a/python/samples/02-agents/providers/github_copilot/github_copilot_with_url.py +++ b/python/samples/02-agents/providers/github_copilot/github_copilot_with_url.py @@ -14,21 +14,20 @@ URL fetching allows the agent to access any URL accessible from your network. import asyncio from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.generated.rpc import PermissionDecisionUserNotAvailable +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session_events import PermissionRequest -def prompt_permission(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: - """Permission handler that prompts the user for approval.""" - print(f"\n[Permission Request: {request.kind}]") - - if request.url is not None: - print(f" URL: {request.url}") - - response = input("Approve? (y/n): ").strip().lower() - if response in ("y", "yes"): - return PermissionRequestResult(kind="approved") - return PermissionRequestResult(kind="denied-interactively-by-user") +def approve_and_log(request: PermissionRequest, context: dict[str, str]) -> PermissionRequestResult: + """Permission handler that approves only URL requests and logs them.""" + if request.kind == "url": + print(f"\n [Permission: {request.kind}]", flush=True) + url = getattr(request, "url", None) + if url is not None: + print(f" URL: {url}", flush=True) + return PermissionHandler.approve_all(request, context) + return PermissionDecisionUserNotAvailable() async def main() -> None: @@ -36,14 +35,14 @@ async def main() -> None: agent = GitHubCopilotAgent( instructions="You are a helpful assistant that can fetch and summarize web content.", - default_options={"on_permission_request": prompt_permission}, + default_options={"on_permission_request": approve_and_log}, ) async with agent: query = "Fetch https://learn.microsoft.com/agent-framework/tutorials/quick-start and summarize its contents" print(f"User: {query}") result = await agent.run(query) - print(f"Agent: {result}\n") + print(f"\nAgent: {result}\n") if __name__ == "__main__": diff --git a/python/scripts/sample_validation/create_dynamic_workflow_executor.py b/python/scripts/sample_validation/create_dynamic_workflow_executor.py index 01af408097..6ebe25a8d4 100644 --- a/python/scripts/sample_validation/create_dynamic_workflow_executor.py +++ b/python/scripts/sample_validation/create_dynamic_workflow_executor.py @@ -14,8 +14,8 @@ from agent_framework import ( handler, ) from agent_framework.github import GitHubCopilotAgent -from copilot.generated.session_events import PermissionRequest -from copilot.session import PermissionRequestResult +from copilot.session import PermissionHandler, PermissionRequestResult +from copilot.session_events import PermissionRequest from pydantic import BaseModel from sample_validation.const import WORKER_COMPLETED from sample_validation.discovery import DiscoveryResult @@ -103,7 +103,7 @@ def prompt_permission( logger.debug( f"[Permission Request: {request.kind}] ({context})Automatically approved for sample validation." ) - return PermissionRequestResult(kind="approved") + return PermissionHandler.approve_all(request, context) class CustomAgentExecutor(Executor): diff --git a/python/uv.lock b/python/uv.lock index 676413c1cc..3203a0016c 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -609,7 +609,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "agent-framework-core", editable = "packages/core" }, - { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = "<=1.0.0b2,>=1.0.0b2" }, + { name = "github-copilot-sdk", marker = "python_full_version >= '3.11'", specifier = ">=1.0.0,<2" }, ] [[package]] @@ -2608,19 +2608,19 @@ wheels = [ [[package]] name = "github-copilot-sdk" -version = "1.0.0b2" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, { name = "python-dateutil", marker = "(python_full_version >= '3.11' and sys_platform == 'darwin') or (python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/82/fe/2cb98d4b9f57f8062ea72775bde72aed1958305016753f7296398e0ceb45/github_copilot_sdk-1.0.0b2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:1b5941d8b6e3d94d42a5bec6607a26f562e6535d5c981089d23d3d224b94601c", size = 67061619, upload-time = "2026-05-06T20:02:08.636Z" }, - { url = "https://files.pythonhosted.org/packages/57/45/76567821b2d36f81e6bca78c98d265e2762733f765fa51d69602b7f81867/github_copilot_sdk-1.0.0b2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b8f6a087a0cf02bb0d33976e8f8c009578d84d701a0b28d52051304791ac70", size = 63790955, upload-time = "2026-05-06T20:02:12.354Z" }, - { url = "https://files.pythonhosted.org/packages/15/67/684b0da0b1207a2bdf025c22ee075d34a1736d61a4973651035d4fd4d8dc/github_copilot_sdk-1.0.0b2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f403638c11b82bddb81c94675fc4e8014a1bb2e86a679a39fa167dcc3ad5416a", size = 69538664, upload-time = "2026-05-06T20:02:16.363Z" }, - { url = "https://files.pythonhosted.org/packages/57/1d/80d88ecf83683535d1a16d4817f1683db3b125f52a924ebdfe9764f5e4c3/github_copilot_sdk-1.0.0b2-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:433d16bb31171fee8d3a5b70259c527f63b297e83a8f8761ae1f16f14d641f32", size = 68163648, upload-time = "2026-05-06T20:02:21.139Z" }, - { url = "https://files.pythonhosted.org/packages/32/d3/b72aa2fbb3194b50b53e8cb1484f5606a1f8eedcdb0bfb5747da52079553/github_copilot_sdk-1.0.0b2-py3-none-win_amd64.whl", hash = "sha256:a6e9782dae4c3c2ab3527b45bb5de0f61998104c10e9ff64698280eaf37ab5dd", size = 62649144, upload-time = "2026-05-06T20:02:24.953Z" }, - { url = "https://files.pythonhosted.org/packages/b6/e2/be95b8ea0ac11d1ca474e28a59284f4e395c2710734eadfb657f5de8ace2/github_copilot_sdk-1.0.0b2-py3-none-win_arm64.whl", hash = "sha256:2e97d0ce4bad67dc5929091cb429e7bbae7d4643e4908a6af256a41439000740", size = 60374365, upload-time = "2026-05-06T20:02:29.02Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d2/e74fdf476d0dde5c3802b3ba360f1b1e250e55d6d39c03f578c28ac9864e/github_copilot_sdk-1.0.0-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:3cae245fb825e26a74395b74f10d9fd90bc464aa77005848ae0809c9a46c96df", size = 94986104, upload-time = "2026-06-02T14:59:55.022Z" }, + { url = "https://files.pythonhosted.org/packages/b6/81/e4d9dd01b0a563e488427aa879166287c88de3fccf7b8a95e22a6c652fc3/github_copilot_sdk-1.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b344a00a877c86ef717244e42bd01acb3694b7377644661c82fc278ccc990e37", size = 91435649, upload-time = "2026-06-02T15:00:02.567Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/e94b8f5a299850e600ffe1fe14bd21b48e01172b9e8b490a0ebd0d0c8d27/github_copilot_sdk-1.0.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:dd3a6b7637a3b12476854aeb599c6bed030f6a166fbd942d872c9a11a695517c", size = 97301959, upload-time = "2026-06-02T15:00:11.019Z" }, + { url = "https://files.pythonhosted.org/packages/4b/bf/dfba743a11d9745b0664ec5e1ae6e05055a5cbef0ccc6d593222319184eb/github_copilot_sdk-1.0.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:bbd2c64fe37016c74620a02d778eaacbd526b4c3b668a3cdff019f831c752eee", size = 96071193, upload-time = "2026-06-02T15:00:22.634Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9b/d953dcbb898f4d44efc0cb592e9a703ad43a4b673aafb5bbd763962ab2fd/github_copilot_sdk-1.0.0-py3-none-win_amd64.whl", hash = "sha256:2d46fff634eece978532b1329c0d9e1d784b08ad521e71e6af06c5c28ae2e7c5", size = 90374124, upload-time = "2026-06-02T15:00:31.376Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f7/0f9943b1439e3dcc52854140676b65d8f63405c471a77c58291a8f4bfb52/github_copilot_sdk-1.0.0-py3-none-win_arm64.whl", hash = "sha256:ebfb80395caa834df8ab16ab4aab3e5d8db883ed3b024f723c394b1514e47221", size = 87874846, upload-time = "2026-06-02T15:00:38.737Z" }, ] [[package]] @@ -2708,6 +2708,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/3f/9859f655d11901e7b2996c6e3d33e0caa9a1d4572c3bc61ed0faa64b2f4c/greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d", size = 277747, upload-time = "2026-02-20T20:16:21.325Z" }, { url = "https://files.pythonhosted.org/packages/fb/07/cb284a8b5c6498dbd7cba35d31380bb123d7dceaa7907f606c8ff5993cbf/greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13", size = 579202, upload-time = "2026-02-20T20:47:28.955Z" }, { url = "https://files.pythonhosted.org/packages/ed/45/67922992b3a152f726163b19f890a85129a992f39607a2a53155de3448b8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e", size = 590620, upload-time = "2026-02-20T20:55:55.581Z" }, + { url = "https://files.pythonhosted.org/packages/03/5f/6e2a7d80c353587751ef3d44bb947f0565ec008a2e0927821c007e96d3a7/greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7", size = 602132, upload-time = "2026-02-20T21:02:43.261Z" }, { url = "https://files.pythonhosted.org/packages/ad/55/9f1ebb5a825215fadcc0f7d5073f6e79e3007e3282b14b22d6aba7ca6cb8/greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f", size = 591729, upload-time = "2026-02-20T20:20:58.395Z" }, { url = "https://files.pythonhosted.org/packages/24/b4/21f5455773d37f94b866eb3cf5caed88d6cea6dd2c6e1f9c34f463cba3ec/greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef", size = 1551946, upload-time = "2026-02-20T20:49:31.102Z" }, { url = "https://files.pythonhosted.org/packages/00/68/91f061a926abead128fe1a87f0b453ccf07368666bd59ffa46016627a930/greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca", size = 1618494, upload-time = "2026-02-20T20:21:06.541Z" }, @@ -2715,6 +2716,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/1430a04657735a3f23116c2e0d5eb10220928846e4537a938a41b350bed6/greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2", size = 605046, upload-time = "2026-02-20T21:02:45.234Z" }, { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, @@ -2723,6 +2725,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c5/cc09412a29e43406eba18d61c70baa936e299bc27e074e2be3806ed29098/greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb", size = 626250, upload-time = "2026-02-20T21:02:46.596Z" }, { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, @@ -2731,6 +2734,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, @@ -2739,6 +2743,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, @@ -2747,6 +2752,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" },