Python: [BREAKING] Upgrade github-copilot-sdk to v1.0.0 (stable) (#6292)

* Python: Upgrade github-copilot-sdk to v1.0.0 (stable)

Upgrade agent-framework-github-copilot from github-copilot-sdk 1.0.0b2 to the
stable 1.0.0 release, adapting to all breaking API changes.

Source changes (_agent.py):
- SubprocessConfig removed: use RuntimeConnection.for_stdio(path=...) +
  CopilotClient kwargs (connection, log_level, base_directory)
- Import paths: copilot.generated.session_events -> copilot.session_events
- Settings: copilot_home -> base_directory (env GITHUB_COPILOT_BASE_DIRECTORY)
- Default deny handler: PermissionDecisionUserNotAvailable() (from
  copilot.generated.rpc)

Test changes:
- Updated imports and client-construction assertions (kwargs-based)
- Permission handler tests use concrete decision types
  (PermissionDecisionApproveOnce, PermissionDecisionDeniedInteractivelyByUser)

Sample changes:
- Permission handlers use PermissionHandler.approve_all or sync
  approve_and_log pattern (v1.0.0 protocol v3 dispatch is incompatible
  with blocking input() in permission handlers)
- Function approval sample uses asyncio.to_thread for interactive prompts
- Simplified imports across all samples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Address PR review: scope permission handlers, widen type, add test

- Shell sample: only approve kind='shell', deny others
- URL sample: only approve kind='url', deny others
- Use getattr() for kind-specific attributes to satisfy pyright
- Widen PermissionHandlerType to accept async handlers (matches SDK)
- Add test for _deny_all_permissions return value

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Fix validation script and strengthen test assertion

- Update scripts/sample_validation/create_dynamic_workflow_executor.py to
  use copilot.session_events imports and PermissionHandler.approve_all
- Assert isinstance(result, PermissionDecisionUserNotAvailable) instead of
  stringly-typed kind check

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Add integration tests for GitHubCopilotAgent

Add 6 integration tests mirroring .NET coverage:
- Basic non-streaming response
- Streaming response
- Function tool invocation
- Session context (multi-turn)
- Session resume by ID
- Shell command execution

Tests require COPILOT_GITHUB_TOKEN env var (skipped otherwise).
Each test cleans up its Copilot session via delete_session.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Giles Odigwe
2026-06-04 01:42:35 -07:00
committed by GitHub
Unverified
parent f970a699d8
commit fe08574a7c
15 changed files with 335 additions and 205 deletions
@@ -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()
@@ -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]
@@ -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)
@@ -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
@@ -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:
@@ -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:
@@ -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()
@@ -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"],
},
)
@@ -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,
},
)
@@ -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__":
@@ -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:
@@ -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__":
@@ -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__":
@@ -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):
+14 -8
View File
@@ -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" },