agent-framework-github-copilot 1.0.3__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-framework-github-copilot
3
- Version: 1.0.3
3
+ Version: 2.0.0
4
4
  Summary: GitHub Copilot integration for Microsoft Agent Framework.
5
5
  Author-email: Microsoft <af-support@microsoft.com>
6
6
  Requires-Python: >=3.11
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
16
16
  Classifier: Typing :: Typed
17
17
  License-File: LICENSE
18
18
  Requires-Dist: agent-framework-core>=1.15.0,<2
19
- Requires-Dist: github-copilot-sdk==1.0.2; python_version >= '3.11'
19
+ Requires-Dist: github-copilot-sdk==1.0.11; python_version >= '3.11'
20
20
  Project-URL: homepage, https://aka.ms/agent-framework
21
21
  Project-URL: issues, https://github.com/microsoft/agent-framework/issues
22
22
  Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
@@ -111,4 +111,33 @@ model described above. When `on_function_approval` is set, it gates `always_requ
111
111
  the default ask-hook is not installed. It is **mutually exclusive** with `on_pre_tool_use` —
112
112
  setting both (whether at construction or per run) raises `ValueError`.
113
113
 
114
+ ## Workspace-driven session options
115
+
116
+ `on_permission_request` and `on_pre_tool_use` gate **tool calls**. They do not cover
117
+ configuration the CLI picks up from the working directory it runs in, which is a separate
118
+ mechanism with its own switches.
119
+
120
+ So that a session behaves the same way in every checkout, `GitHubCopilotAgent` leaves the
121
+ following off by default:
122
+
123
+ | Option | Default | Effect when enabled |
124
+ | --- | --- | --- |
125
+ | `enable_file_hooks` | `False` | The CLI loads file hooks from the working directory's `.github/hooks/` and runs the commands they define, independently of the tool-approval path. |
126
+
127
+ Opt in per agent or per run when your workflow needs the checkout to drive the session:
128
+
129
+ ```python
130
+ agent = GitHubCopilotAgent(
131
+ default_options=GitHubCopilotOptions(enable_file_hooks=True),
132
+ )
133
+ ```
134
+
135
+ Only enable these for a working directory whose contents you trust to act on the host.
136
+
137
+ To make the default visible rather than silent, the agent logs a warning through the
138
+ `agent_framework.github_copilot` logger the first time it starts a session in a working
139
+ directory that defines hooks it is not loading. See
140
+ [`github_copilot_with_file_hooks.py`](../../samples/02-agents/providers/github_copilot/github_copilot_with_file_hooks.py)
141
+ for a runnable example.
142
+
114
143
 
@@ -87,3 +87,32 @@ model described above. When `on_function_approval` is set, it gates `always_requ
87
87
  the default ask-hook is not installed. It is **mutually exclusive** with `on_pre_tool_use` —
88
88
  setting both (whether at construction or per run) raises `ValueError`.
89
89
 
90
+ ## Workspace-driven session options
91
+
92
+ `on_permission_request` and `on_pre_tool_use` gate **tool calls**. They do not cover
93
+ configuration the CLI picks up from the working directory it runs in, which is a separate
94
+ mechanism with its own switches.
95
+
96
+ So that a session behaves the same way in every checkout, `GitHubCopilotAgent` leaves the
97
+ following off by default:
98
+
99
+ | Option | Default | Effect when enabled |
100
+ | --- | --- | --- |
101
+ | `enable_file_hooks` | `False` | The CLI loads file hooks from the working directory's `.github/hooks/` and runs the commands they define, independently of the tool-approval path. |
102
+
103
+ Opt in per agent or per run when your workflow needs the checkout to drive the session:
104
+
105
+ ```python
106
+ agent = GitHubCopilotAgent(
107
+ default_options=GitHubCopilotOptions(enable_file_hooks=True),
108
+ )
109
+ ```
110
+
111
+ Only enable these for a working directory whose contents you trust to act on the host.
112
+
113
+ To make the default visible rather than silent, the agent logs a warning through the
114
+ `agent_framework.github_copilot` logger the first time it starts a session in a working
115
+ directory that defines hooks it is not loading. See
116
+ [`github_copilot_with_file_hooks.py`](../../samples/02-agents/providers/github_copilot/github_copilot_with_file_hooks.py)
117
+ for a runnable example.
118
+
@@ -10,6 +10,7 @@ import logging
10
10
  import sys
11
11
  import warnings
12
12
  from collections.abc import AsyncIterable, Awaitable, Callable, Mapping, MutableMapping, Sequence
13
+ from pathlib import Path
13
14
  from typing import Any, ClassVar, Generic, Literal, TypedDict, cast, overload
14
15
  from urllib.parse import urlparse
15
16
 
@@ -30,6 +31,7 @@ from agent_framework import (
30
31
  add_usage_details,
31
32
  normalize_messages,
32
33
  )
34
+ from agent_framework._mcp import MCPTool
33
35
  from agent_framework._settings import load_settings
34
36
  from agent_framework._telemetry import mark_feature_used
35
37
  from agent_framework._tools import FunctionTool, ToolTypes
@@ -77,6 +79,7 @@ try:
77
79
  Attachment,
78
80
  BlobAttachment,
79
81
  MCPServerConfig,
82
+ PermissionInvocation,
80
83
  PermissionRequestResult,
81
84
  PreToolUseHandler,
82
85
  PreToolUseHookOutput,
@@ -110,14 +113,24 @@ except ImportError as _copilot_import_error:
110
113
  DEFAULT_TIMEOUT_SECONDS: float = 60.0
111
114
  """Default timeout in seconds for Copilot requests."""
112
115
 
116
+ _PermissionHandlerContext = Any
117
+ """Compatibility context accepted by permission handlers across SDK versions."""
118
+
113
119
  PermissionHandlerType = Callable[
114
- [PermissionRequest, dict[str, str]], "PermissionRequestResult | Awaitable[PermissionRequestResult]"
120
+ [PermissionRequest, _PermissionHandlerContext],
121
+ "PermissionRequestResult | Awaitable[PermissionRequestResult]",
115
122
  ]
116
123
  """Type for permission request handlers. Supports both sync and async callbacks."""
117
124
 
118
- AsyncPermissionHandlerType = Callable[[PermissionRequest, dict[str, str]], "Awaitable[PermissionRequestResult]"]
125
+ AsyncPermissionHandlerType = Callable[
126
+ [PermissionRequest, _PermissionHandlerContext], "Awaitable[PermissionRequestResult]"
127
+ ]
119
128
  """Type for permission request handlers that are always asynchronous."""
120
129
 
130
+ _SdkAsyncPermissionHandlerType = Callable[
131
+ [PermissionRequest, PermissionInvocation], "Awaitable[PermissionRequestResult]"
132
+ ]
133
+
121
134
 
122
135
  FunctionApprovalCallback = Callable[[Content], "bool | Awaitable[bool]"]
123
136
  """Deprecated approval callback for ``FunctionTool`` instances declared with
@@ -168,10 +181,26 @@ async def _resolve_function_approval(
168
181
 
169
182
  logger = logging.getLogger("agent_framework.github_copilot")
170
183
 
184
+ _MCP_TOOL_MESSAGE = (
185
+ "MCP server '{name}' cannot be passed to GitHubCopilotAgent as a tool: the Copilot SDK "
186
+ "connects to MCP servers itself, so a framework-managed MCPTool would keep none of its "
187
+ "framework behavior. Configure the server natively instead, for example "
188
+ "default_options={{'mcp_servers': {{'{name}': {{'type': 'stdio', 'command': 'python', "
189
+ "'args': ['server.py'], 'tools': ['*']}}}}}}, or use a ChatAgent, where the framework owns "
190
+ "the connection."
191
+ )
192
+
193
+
194
+ def _reject_mcp_tools(tools: Sequence[Any]) -> None:
195
+ """Refuse MCP servers handed in as tools, from whichever option carried them."""
196
+ for tool in tools:
197
+ if isinstance(tool, MCPTool):
198
+ raise TypeError(_MCP_TOOL_MESSAGE.format(name=tool.name))
199
+
171
200
 
172
201
  def _deny_all_permissions(
173
202
  _request: PermissionRequest,
174
- _invocation: dict[str, str],
203
+ _invocation: _PermissionHandlerContext,
175
204
  ) -> PermissionRequestResult:
176
205
  """Default permission handler that denies all requests."""
177
206
  return PermissionDecisionUserNotAvailable()
@@ -322,7 +351,7 @@ def _normalize_permission_decision(
322
351
  return PermissionDecisionApproveForSession(approval=approval)
323
352
 
324
353
 
325
- def _with_normalized_permission_decisions(handler: PermissionHandlerType) -> AsyncPermissionHandlerType:
354
+ def _with_normalized_permission_decisions(handler: PermissionHandlerType) -> _SdkAsyncPermissionHandlerType:
326
355
  """Wrap a permission handler so its decisions are normalized before reaching the SDK.
327
356
 
328
357
  Exceptions raised by ``handler`` deliberately propagate: the SDK already catches them
@@ -335,8 +364,10 @@ def _with_normalized_permission_decisions(handler: PermissionHandlerType) -> Asy
335
364
  An async handler delegating to ``handler`` and normalizing its result.
336
365
  """
337
366
 
338
- async def normalized_handler(request: PermissionRequest, invocation: dict[str, str]) -> PermissionRequestResult:
339
- result = handler(request, invocation)
367
+ async def normalized_handler(
368
+ request: PermissionRequest, invocation: PermissionInvocation
369
+ ) -> PermissionRequestResult:
370
+ result = handler(request, cast(PermissionInvocation, dict(invocation)))
340
371
  if inspect.isawaitable(result):
341
372
  result = await result
342
373
  return _normalize_permission_decision(result, request)
@@ -364,6 +395,45 @@ def _parse_telemetry_config(raw: str) -> TelemetryConfig | None:
364
395
  return cast(TelemetryConfig, parsed)
365
396
 
366
397
 
398
+ def _client_working_directory(client: CopilotClient | None) -> str | None:
399
+ """Best-effort read of the working directory an injected client configured.
400
+
401
+ A client created with ``CopilotClient(working_directory=...)`` spawns the CLI process
402
+ in that directory, so the CLI resolves ``.github/hooks/`` relative to it rather than to
403
+ this process. The SDK keeps the value on a private options object, so this is read
404
+ defensively: if the attribute ever moves, hook detection falls back to the process
405
+ working directory instead of failing.
406
+ """
407
+ options = getattr(client, "_options", None)
408
+ working_directory = getattr(options, "working_directory", None)
409
+ return working_directory if isinstance(working_directory, str) else None
410
+
411
+
412
+ def _resolve_effective_working_directory(session_working_directory: str | None, client: CopilotClient | None) -> Path:
413
+ """Resolve the directory the CLI will treat as the workspace, mirroring the SDK.
414
+
415
+ The SDK resolves in this order: the session's ``working_directory`` when supplied,
416
+ otherwise the CLI process's own directory, which is the injected client's configured
417
+ ``working_directory`` when it set one and this process's directory otherwise.
418
+ """
419
+ if session_working_directory:
420
+ return Path(session_working_directory)
421
+ client_working_directory = _client_working_directory(client)
422
+ if client_working_directory:
423
+ return Path(client_working_directory)
424
+ return Path.cwd()
425
+
426
+
427
+ def _has_file_hooks(working_directory: Path) -> bool:
428
+ """Return whether ``.github/hooks/`` holds hook definitions the CLI would otherwise load."""
429
+ try:
430
+ return any((working_directory / ".github" / "hooks").iterdir())
431
+ except OSError:
432
+ # Missing directory, unreadable path, or a file where the directory would be:
433
+ # there is nothing the caller needs to know about.
434
+ return False
435
+
436
+
367
437
  class GitHubCopilotSettings(TypedDict, total=False):
368
438
  """GitHub Copilot model settings.
369
439
 
@@ -468,6 +538,14 @@ class GitHubCopilotOptions(TypedDict, total=False):
468
538
  base_directory: str
469
539
  """Directory where the CLI stores session state, configuration, and other persistent data."""
470
540
 
541
+ enable_file_hooks: bool
542
+ """Whether the CLI loads file hooks from the working directory's ``.github/hooks/``.
543
+
544
+ Defaults to ``False``: hook definitions checked into the working directory are ignored
545
+ unless you opt in, so a session behaves the same way regardless of which checkout it
546
+ runs in. Unrelated to the SDK callback hooks configured through ``on_pre_tool_use``.
547
+ """
548
+
471
549
  telemetry: TelemetryConfig
472
550
  """OpenTelemetry configuration for the Copilot CLI process."""
473
551
 
@@ -641,6 +719,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
641
719
  )
642
720
 
643
721
  self._tools = normalize_tools(tools)
722
+ _reject_mcp_tools(self._tools)
644
723
  self._permission_handler = on_permission_request
645
724
  self._on_pre_tool_use: PreToolUseHandler | None = on_pre_tool_use
646
725
  self._function_approval_handler: FunctionApprovalCallback | None = on_function_approval
@@ -649,6 +728,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
649
728
  # are forwarded verbatim to the Copilot SDK by _build_session_kwargs.
650
729
  self._default_options = opts
651
730
  self._started = False
731
+ self._file_hooks_warning_emitted = False
652
732
 
653
733
  async def __aenter__(self) -> Self:
654
734
  """Start the agent when entering async context."""
@@ -1428,6 +1508,27 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1428
1508
  except Exception as ex:
1429
1509
  raise AgentException(f"Failed to create GitHub Copilot session: {ex}") from ex
1430
1510
 
1511
+ def _warn_once_about_unloaded_file_hooks(self, session_working_directory: str | None) -> None:
1512
+ """Warn once when the effective working directory defines file hooks that will not run.
1513
+
1514
+ Without this the default is silent: hooks simply stop running, with nothing to
1515
+ point at the cause. The warning fires at most once per agent so a long-lived agent
1516
+ does not repeat it on every run.
1517
+ """
1518
+ if self._file_hooks_warning_emitted:
1519
+ return
1520
+ working_directory = _resolve_effective_working_directory(session_working_directory, self._client)
1521
+ if not _has_file_hooks(working_directory):
1522
+ return
1523
+ self._file_hooks_warning_emitted = True
1524
+ logger.warning(
1525
+ "Not loading the file hooks defined in '%s': GitHubCopilotAgent leaves "
1526
+ "enable_file_hooks off so a session behaves the same way in every working "
1527
+ "directory. Set enable_file_hooks=True in default_options (or in per-run "
1528
+ "options) to run them.",
1529
+ working_directory / ".github" / "hooks",
1530
+ )
1531
+
1431
1532
  def _build_session_kwargs(
1432
1533
  self,
1433
1534
  streaming: bool,
@@ -1439,11 +1540,12 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1439
1540
  ``runtime_options`` which override them. Every key is forwarded verbatim to
1440
1541
  the Copilot SDK, so any ``create_session`` parameter is supported without a
1441
1542
  dedicated mapping here (an unknown name surfaces as a ``TypeError`` from the
1442
- SDK). A few keys are handled specially because they need a secure default
1443
- (``on_permission_request`` defaults to denying all requests, and is wrapped so
1444
- under-specified ``approve-for-session`` decisions are scoped to the request that
1445
- triggered them) or transforming: ``tools`` are merged with the agent's tools and
1446
- converted to SDK tools, and approval callbacks are turned into ``hooks``.
1543
+ SDK). A few keys are handled specially because they need a specific default or
1544
+ transforming: ``on_permission_request`` defaults to denying all requests and is
1545
+ wrapped so under-specified ``approve-for-session`` decisions are scoped to the
1546
+ request that triggered them, ``enable_file_hooks`` defaults to off, ``tools`` are
1547
+ merged with the agent's tools and converted to SDK tools, and approval callbacks
1548
+ are turned into ``hooks``.
1447
1549
 
1448
1550
  Args:
1449
1551
  streaming: Whether to enable streaming for the session.
@@ -1459,7 +1561,10 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1459
1561
 
1460
1562
  # Merge agent-level tools with any caller-supplied tools (from default_options
1461
1563
  # or per-run options, the latter winning) and convert to SDK tools.
1462
- all_tools = list(self._tools or []) + list(kwargs.get("tools") or [])
1564
+ # Normalize the option-supplied tools the way the constructor does: it converts callables
1565
+ # and flattens tool-collection wrappers, which can otherwise hide an MCPTool.
1566
+ all_tools = normalize_tools(list(self._tools or []) + list(kwargs.get("tools") or []))
1567
+ _reject_mcp_tools(all_tools)
1463
1568
  kwargs["tools"] = self._prepare_tools(all_tools) if all_tools else None
1464
1569
 
1465
1570
  kwargs["streaming"] = streaming
@@ -1468,8 +1573,17 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1468
1573
  if not kwargs.get("model"):
1469
1574
  kwargs["model"] = self._settings.get("model") or None
1470
1575
  kwargs["on_permission_request"] = _with_normalized_permission_decisions(
1471
- opts.get("on_permission_request") or self._permission_handler or _deny_all_permissions
1576
+ cast(
1577
+ PermissionHandlerType,
1578
+ opts.get("on_permission_request") or self._permission_handler or _deny_all_permissions,
1579
+ )
1472
1580
  )
1581
+ # File hooks let the working directory's checked-in configuration influence what the
1582
+ # CLI does on the host, so the agent leaves them off for a consistent session in every
1583
+ # checkout. Callers opt in through ``default_options`` or per-run options.
1584
+ if kwargs.get("enable_file_hooks") is None:
1585
+ kwargs["enable_file_hooks"] = False
1586
+ self._warn_once_about_unloaded_file_hooks(kwargs.get("working_directory"))
1473
1587
  kwargs["hooks"] = self._build_session_hooks(all_tools, kwargs)
1474
1588
 
1475
1589
  # Strip agent-internal and client-level keys that are consumed here or in the
@@ -4,7 +4,7 @@ description = "GitHub Copilot integration for Microsoft Agent Framework."
4
4
  authors = [{ name = "Microsoft", email = "af-support@microsoft.com"}]
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
7
- version = "1.0.3"
7
+ version = "2.0.0"
8
8
  license-files = ["LICENSE"]
9
9
  urls.homepage = "https://aka.ms/agent-framework"
10
10
  urls.source = "https://github.com/microsoft/agent-framework/tree/main/python"
@@ -23,11 +23,11 @@ classifiers = [
23
23
  ]
24
24
  dependencies = [
25
25
  "agent-framework-core>=1.15.0,<2",
26
- "github-copilot-sdk==1.0.2; python_version >= '3.11'",
26
+ "github-copilot-sdk==1.0.11; python_version >= '3.11'",
27
27
  ]
28
28
 
29
29
  [tool.uv]
30
- prerelease = "if-necessary-or-explicit"
30
+ prerelease = "if-necessary"
31
31
  environments = [
32
32
  "sys_platform == 'darwin'",
33
33
  "sys_platform == 'linux'",