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
@@ -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__":