agent-framework-github-copilot 2.1.0__py3-none-any.whl → 2.1.2__py3-none-any.whl

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.
@@ -21,8 +21,7 @@ from collections.abc import (
21
21
  Sequence,
22
22
  )
23
23
  from pathlib import Path
24
- from typing import Any, ClassVar, Generic, Literal, TypedDict, cast, overload
25
- from urllib.parse import urlparse
24
+ from typing import Any, ClassVar, Generic, Literal, Self, TypedDict, cast, overload
26
25
 
27
26
  from agent_framework import (
28
27
  AgentMiddlewareLayer,
@@ -43,6 +42,7 @@ from agent_framework import (
43
42
  normalize_messages,
44
43
  )
45
44
  from agent_framework._mcp import MCPTool
45
+ from agent_framework._middleware import categorize_middleware
46
46
  from agent_framework._settings import load_settings
47
47
  from agent_framework._telemetry import mark_feature_used
48
48
  from agent_framework._tools import FunctionTool, ToolTypes
@@ -51,15 +51,11 @@ from agent_framework._types import (
51
51
  _get_data_bytes_as_str, # pyright: ignore[reportPrivateUsage]
52
52
  normalize_tools,
53
53
  )
54
- from agent_framework.exceptions import AgentException, ContentError
54
+ from agent_framework.exceptions import AgentException, ContentError, MiddlewareException
55
55
  from agent_framework.observability import AgentTelemetryLayer
56
56
 
57
57
  from ._feature_usage import FeatureIndex
58
58
 
59
- if sys.version_info >= (3, 11):
60
- from typing import Self # pragma: no cover
61
- else:
62
- from typing_extensions import Self # pragma: no cover
63
59
  if sys.version_info >= (3, 13):
64
60
  from typing import TypeVar # pragma: no cover
65
61
  else:
@@ -271,41 +267,6 @@ def _derive_session_approval(request: PermissionRequest) -> PermissionDecisionAp
271
267
  return None
272
268
 
273
269
 
274
- # Characters the WHATWG URL parser (used by the Copilot CLI) treats specially for
275
- # special-scheme URLs in ways that can move the authority boundary: backslashes are
276
- # normalized to forward slashes, and tabs/newlines/carriage returns are stripped before
277
- # parsing. Python's ``urlparse`` does none of this, so a URL containing any of them may
278
- # resolve to a different host than the CLI actually contacts.
279
- _WHATWG_AMBIGUOUS_URL_CHARS = ("\\", "\t", "\n", "\r")
280
-
281
-
282
- def _derive_url_session_domain(url: str) -> str | None:
283
- """Return the domain to persist for a URL prompt, or ``None`` when it is unsafe to.
284
-
285
- The persisted domain must match the host the Copilot CLI actually contacts, but the CLI
286
- parses URLs with WHATWG semantics while this runs on Python's ``urlparse``. The two
287
- disagree on crafted authorities -- a backslash before the ``@`` in
288
- ``https://example.com<backslash>@evil.com`` resolves to ``example.com`` under the CLI
289
- but ``evil.com`` under ``urlparse`` -- so trusting ``urlparse`` here could persist a
290
- session-wide approval for an unrelated, attacker-chosen domain.
291
-
292
- To keep the "narrow, never widen" guarantee, the domain is only returned when the URL
293
- contains none of the characters the two parsers handle differently; any ambiguity (or a
294
- URL with no derivable host) yields ``None`` so the caller can approve the single request
295
- without persisting a domain.
296
-
297
- Args:
298
- url: The URL from the permission request.
299
-
300
- Returns:
301
- The lower-cased host to approve for the session, or ``None`` when the URL is
302
- parser-ambiguous or has no host.
303
- """
304
- if any(char in url for char in _WHATWG_AMBIGUOUS_URL_CHARS):
305
- return None
306
- return urlparse(url).hostname or None
307
-
308
-
309
270
  def _normalize_permission_decision(
310
271
  decision: PermissionRequestResult,
311
272
  request: PermissionRequest,
@@ -317,11 +278,12 @@ def _normalize_permission_decision(
317
278
  is constructible with neither. That serializes to ``{"kind": "approve-for-session"}``,
318
279
  which the Copilot CLI cannot interpret -- it crashes with ``Cannot read properties of
319
280
  undefined (reading 'commandIdentifiers')``, taking the whole run down with it. This
320
- reconstructs the intended scope from ``request``.
281
+ reconstructs structured tool scopes from ``request`` and narrows to a single request
282
+ when an exact session scope cannot be safely inferred.
321
283
 
322
- The decision is only ever narrowed, never widened: when the prompt does not offer
323
- session-scoped approval, or the request kind has no session approval representation,
324
- the decision is downgraded to a single-use approval.
284
+ The decision is only ever narrowed, never widened: URL prompts, prompts that do not
285
+ offer session-scoped approval, and request kinds with no session approval representation
286
+ are downgraded to a single-use approval.
325
287
 
326
288
  Args:
327
289
  decision: The decision returned by the caller's permission handler.
@@ -339,14 +301,11 @@ def _normalize_permission_decision(
339
301
 
340
302
  try:
341
303
  if isinstance(request, PermissionRequestUrl):
342
- domain = _derive_url_session_domain(request.url)
343
- if domain:
344
- return PermissionDecisionApproveForSession(domain=domain)
345
304
  logger.warning(
346
305
  "Permission handler returned an unscoped 'approve-for-session' decision for a URL prompt, "
347
- "but no unambiguous domain could be derived from '%s'. Approving this request only. Return "
306
+ "but Agent Framework cannot safely derive the Copilot CLI's exact URL permission scope. "
307
+ "Approving this request only. Return "
348
308
  "PermissionDecisionApproveForSession(domain=...) to approve a domain for the session.",
349
- request.url,
350
309
  )
351
310
  return PermissionDecisionApproveOnce()
352
311
 
@@ -895,6 +854,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
895
854
 
896
855
  Raises:
897
856
  AgentException: If the request fails.
857
+ MiddlewareException: If a context provider contributes unsupported function middleware.
898
858
  """
899
859
  if middleware:
900
860
  logger.warning(
@@ -1102,6 +1062,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1102
1062
 
1103
1063
  Raises:
1104
1064
  AgentException: If the request fails.
1065
+ MiddlewareException: If a context provider contributes unsupported function middleware.
1105
1066
  """
1106
1067
  if not self._started:
1107
1068
  await self.start()
@@ -1168,7 +1129,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1168
1129
  contents=[
1169
1130
  Content.from_usage(
1170
1131
  usage_details,
1171
- merge_mode="replace",
1132
+ merge_mode="append",
1172
1133
  raw_representation=event.data,
1173
1134
  )
1174
1135
  ]
@@ -1268,6 +1229,10 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1268
1229
 
1269
1230
  Returns:
1270
1231
  The SessionContext with provider context populated.
1232
+
1233
+ Raises:
1234
+ MiddlewareException: If a context provider contributes function middleware,
1235
+ which cannot be applied to the SDK-owned tool loop.
1271
1236
  """
1272
1237
  session_context = SessionContext(
1273
1238
  session_id=session.session_id,
@@ -1286,6 +1251,12 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1286
1251
  state=session.state.setdefault(provider.source_id, {}),
1287
1252
  )
1288
1253
 
1254
+ if categorize_middleware(session_context.get_middleware())["function"]:
1255
+ raise MiddlewareException(
1256
+ "GitHubCopilotAgent does not support function middleware contributed by context providers. "
1257
+ "Use Agent with a chat client when function middleware is required."
1258
+ )
1259
+
1289
1260
  return session_context
1290
1261
 
1291
1262
  @staticmethod
@@ -1505,17 +1476,16 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1505
1476
  ``approval_mode="always_require"`` is delegated to the Copilot SDK's native
1506
1477
  ``on_pre_tool_use`` hook:
1507
1478
 
1508
- - If the caller supplies their own session hooks -- either the SDK-native
1509
- ``hooks`` dict or the convenience ``on_pre_tool_use`` handler (via per-run
1510
- ``options`` or ``default_options``) -- those take precedence and are used
1511
- as-is. When both are given, the explicit ``hooks`` dict wins for any key it
1512
- defines and the ``on_pre_tool_use`` shortcut fills in that key otherwise. A
1513
- warning is logged naming any approval-required tool that will therefore not
1514
- be automatically gated, since the caller's hooks are responsible for
1515
- enforcing approval.
1479
+ - If the caller supplies their own ``on_pre_tool_use`` handler -- either in
1480
+ the SDK-native ``hooks`` dict or through the convenience option (via per-run
1481
+ ``options`` or ``default_options``) -- it takes precedence. When both are
1482
+ given, the explicit ``hooks`` dict wins for any key it defines and the
1483
+ shortcut fills in that key otherwise. A warning names any approval-required
1484
+ tool that the caller's pre-tool-use handler is responsible for gating.
1516
1485
  - Otherwise, when any approval-required tool is present, a default hook is
1517
1486
  installed that returns ``"ask"`` for those tools (routing the decision to
1518
1487
  ``on_permission_request``) and defers (``None``) for all other tools.
1488
+ Unrelated caller hooks are preserved and do not replace this default.
1519
1489
  - The default hook is **not** installed when the deprecated
1520
1490
  ``on_function_approval`` callback is configured: in that case approval is
1521
1491
  enforced inside the tool handler (see :meth:`_tool_to_copilot_tool`) to
@@ -1532,12 +1502,12 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1532
1502
  The hooks to register for the session, or ``None`` if none are needed.
1533
1503
  """
1534
1504
  user_hook: PreToolUseHandler | None = options.get("on_pre_tool_use") or self._on_pre_tool_use
1535
- caller_hooks: Mapping[str, Any] | None = options.get("hooks")
1505
+ caller_hooks: SessionHooks | None = options.get("hooks")
1536
1506
 
1537
1507
  # Combine caller-provided hooks: the SDK-native ``hooks`` dict plus the
1538
1508
  # convenience ``on_pre_tool_use`` shortcut. The explicit dict wins for the
1539
1509
  # keys it defines; the shortcut only fills in ``on_pre_tool_use`` otherwise.
1540
- combined: dict[str, Any] = {}
1510
+ combined: SessionHooks = {}
1541
1511
  if user_hook is not None:
1542
1512
  combined["on_pre_tool_use"] = user_hook
1543
1513
  if caller_hooks:
@@ -1547,7 +1517,7 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1547
1517
  tool.name for tool in all_tools if isinstance(tool, FunctionTool) and tool.approval_mode == "always_require"
1548
1518
  }
1549
1519
 
1550
- if combined:
1520
+ if combined.get("on_pre_tool_use") is not None:
1551
1521
  if approval_required_names:
1552
1522
  logger.warning(
1553
1523
  "Custom session hooks are configured, so %d approval-required tool(s) (%s) "
@@ -1556,15 +1526,15 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1556
1526
  len(approval_required_names),
1557
1527
  ", ".join(sorted(approval_required_names)),
1558
1528
  )
1559
- return cast("SessionHooks", combined)
1529
+ return combined
1560
1530
 
1561
1531
  if not approval_required_names:
1562
- return None
1532
+ return combined or None
1563
1533
 
1564
1534
  # The deprecated on_function_approval callback enforces approval in the tool
1565
1535
  # handler; don't also install the default ask-hook (which would double-gate).
1566
1536
  if self._function_approval_handler is not None:
1567
- return None
1537
+ return combined or None
1568
1538
 
1569
1539
  def default_pre_tool_use(
1570
1540
  hook_input: Mapping[str, Any],
@@ -1580,7 +1550,8 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1580
1550
  }
1581
1551
  return None
1582
1552
 
1583
- return {"on_pre_tool_use": default_pre_tool_use}
1553
+ combined["on_pre_tool_use"] = default_pre_tool_use
1554
+ return combined
1584
1555
 
1585
1556
  def _session_run_lock(self, service_session_id: str) -> asyncio.Lock:
1586
1557
  """Return the lock that serializes runs on one Copilot service session."""
@@ -1735,6 +1706,11 @@ class RawGitHubCopilotAgent(BaseAgent, Generic[OptionsT]):
1735
1706
  merged with the agent's tools and converted to SDK tools, and approval callbacks
1736
1707
  are turned into ``hooks``.
1737
1708
 
1709
+ Forwarding options alone is not an authorization or revocation guarantee for
1710
+ capabilities already resident in a resumed SDK/CLI service session. Enforce
1711
+ authorization at the MCP service/tool boundary and use a fresh service session
1712
+ when narrowing configured capabilities.
1713
+
1738
1714
  Args:
1739
1715
  streaming: Whether to enable streaming for the session.
1740
1716
  runtime_options: Runtime options that take precedence over default_options.
@@ -1873,10 +1849,10 @@ class GitHubCopilotAgent( # type: ignore[misc]
1873
1849
  RawGitHubCopilotAgent[OptionsT],
1874
1850
  Generic[OptionsT],
1875
1851
  ):
1876
- """A GitHub Copilot Agent with full middleware and telemetry support.
1852
+ """A GitHub Copilot Agent with agent middleware and telemetry support.
1877
1853
 
1878
1854
  This is the recommended agent class for most use cases. It includes
1879
- middleware support and OpenTelemetry-based telemetry for observability,
1855
+ agent middleware and OpenTelemetry-based telemetry for observability,
1880
1856
  with middleware running outside the telemetry span so middleware execution
1881
1857
  time is not captured in traces. For a minimal implementation without these
1882
1858
  layers, use :class:`RawGitHubCopilotAgent`.
@@ -1926,7 +1902,7 @@ class GitHubCopilotAgent( # type: ignore[misc]
1926
1902
  env_file_path: str | None = None,
1927
1903
  env_file_encoding: str | None = None,
1928
1904
  ) -> None:
1929
- """Initialize a GitHub Copilot Agent with full middleware and telemetry.
1905
+ """Initialize a GitHub Copilot Agent with agent middleware and telemetry.
1930
1906
 
1931
1907
  Args:
1932
1908
  instructions: System message for the agent.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-framework-github-copilot
3
- Version: 2.1.0
3
+ Version: 2.1.2
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
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.15
17
17
  Classifier: Typing :: Typed
18
18
  License-File: LICENSE
19
19
  Requires-Dist: agent-framework-core>=1.22.0,<2
20
- Requires-Dist: github-copilot-sdk>=1.0.16,<2.0; python_version >= '3.11'
20
+ Requires-Dist: github-copilot-sdk>=1.0.16,<2.0
21
21
  Project-URL: homepage, https://aka.ms/agent-framework
22
22
  Project-URL: issues, https://github.com/microsoft/agent-framework/issues
23
23
  Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
@@ -36,6 +36,17 @@ pip install agent-framework-github-copilot
36
36
 
37
37
  The GitHub Copilot agent enables integration with GitHub Copilot, allowing you to interact with Copilot's agentic capabilities through the Agent Framework.
38
38
 
39
+ ## Context providers and function middleware
40
+
41
+ Context providers can contribute messages, instructions, and tools to a run.
42
+ `GitHubCopilotAgent` supports agent middleware, but the SDK owns its tool-calling loop
43
+ and does not run Agent Framework function middleware. If a context provider contributes
44
+ function middleware through `SessionContext.extend_middleware()`, the run raises
45
+ `MiddlewareException` before creating or resuming an SDK session or registering its tools.
46
+ This applies to both response modes and to `RawGitHubCopilotAgent`.
47
+
48
+ Use `Agent` with a chat client when provider-contributed function middleware is required.
49
+
39
50
  ## Tool approval (`approval_mode="always_require"`)
40
51
 
41
52
  The GitHub Copilot SDK owns the tool-calling loop for this provider, so approval for
@@ -46,7 +57,8 @@ When you register a `FunctionTool` declared with `approval_mode="always_require"
46
57
  do **not** supply your own `on_pre_tool_use` hook, `GitHubCopilotAgent` installs a default
47
58
  `on_pre_tool_use` hook that returns `"ask"` for that tool and defers (`None`) for all other
48
59
  tools. The `"ask"` decision routes to your `on_permission_request` handler, where you
49
- approve or deny the call:
60
+ approve or deny the call. Other session hooks, such as `on_post_tool_use` or
61
+ `on_session_start`, are preserved alongside this default approval hook:
50
62
 
51
63
  ```python
52
64
  from agent_framework import tool
@@ -83,10 +95,9 @@ prompts) or a `domain` (URL prompts). Both are optional, so a bare
83
95
  `PermissionDecisionApproveForSession()` carries no scope at all and the Copilot CLI cannot
84
96
  interpret it.
85
97
 
86
- `GitHubCopilotAgent` therefore scopes such a decision automatically, using the request that
87
- triggered it — a shell prompt becomes an approval for that prompt's command identifiers, an
88
- MCP prompt an approval for that server and tool, a URL prompt an approval for that URL's
89
- domain, and so on:
98
+ `GitHubCopilotAgent` therefore scopes such a decision automatically when the request provides
99
+ a structured session approval — a shell prompt becomes an approval for that prompt's command
100
+ identifiers, an MCP prompt an approval for that server and tool, and so on:
90
101
 
91
102
  ```python
92
103
  from copilot.generated.rpc import PermissionDecisionApproveForSession
@@ -97,12 +108,13 @@ def on_permission_request(request, invocation):
97
108
  return PermissionDecisionApproveForSession()
98
109
  ```
99
110
 
100
- The decision is only ever narrowed, never widened. When the prompt reports that it cannot
101
- offer session-scoped approval (`can_offer_session_approval=False`), or the request kind has
102
- no session-scoped approval at all (such as a `hook` prompt), the decision is downgraded to a
103
- single-use approval and a warning is logged. Pass an explicit `approval=` or `domain=` when
104
- you want to approve something other than the request being handled — decisions that already
105
- specify a scope are forwarded unchanged.
111
+ The decision is only ever narrowed, never widened. A bare decision for a URL prompt is
112
+ downgraded to a single-use approval because the framework cannot safely infer the Copilot
113
+ CLI's exact URL permission scope. The same downgrade occurs when the prompt reports that it
114
+ cannot offer session-scoped approval (`can_offer_session_approval=False`), or the request
115
+ kind has no session-scoped approval at all (such as a `hook` prompt). A warning is logged in
116
+ each case. Pass an explicit `approval=` or `domain=` when you want to approve a specific
117
+ scope for the session — decisions that already specify a scope are forwarded unchanged.
106
118
 
107
119
  ### Deprecated: `on_function_approval`
108
120
 
@@ -142,4 +154,20 @@ directory that defines hooks it is not loading. See
142
154
  [`github_copilot_with_file_hooks.py`](../../samples/02-agents/providers/github_copilot/github_copilot_with_file_hooks.py)
143
155
  for a runnable example.
144
156
 
157
+ ## MCP configuration and session lifetime
158
+
159
+ Constructor `default_options` provide SDK session defaults. Per-run `options` override
160
+ the corresponding defaults for that request, including `mcp_servers`. Reusing an
161
+ `AgentSession` with a `service_session_id` resumes SDK/CLI-owned service-session state.
162
+ Forwarding a narrower MCP configuration, such as changing a server's `tools` from
163
+ `["*"]` to `[]`, is not an authorization or revocation guarantee for capabilities
164
+ already resident in that session. Enforce authorization at the MCP service/tool boundary.
165
+
166
+ Use a fresh service session when narrowing configured capabilities: create
167
+ `fresh_session = agent.create_session()`, then pass it to
168
+ `await agent.run("Start a new conversation", session=fresh_session, options=restricted_options)`,
169
+ where `restricted_options` contains the narrowed `mcp_servers` configuration.
170
+ The new session has no `service_session_id`; do not copy the previous service-session
171
+ state or reuse its SDK `session_id` in constructor or per-run options. A fresh session
172
+ starts a new conversation rather than continuing the previous one.
145
173
 
@@ -0,0 +1,8 @@
1
+ agent_framework_github_copilot/__init__.py,sha256=A8xc4tEW9aFASMSwBmiWIn6MWnOU1rXGyN3HMzEOZxw,464
2
+ agent_framework_github_copilot/_agent.py,sha256=SKTS7dj9t6igmpkwZ3nMsGqREBQiWsHNFV1-XUZTRTU,83980
3
+ agent_framework_github_copilot/_feature_usage.py,sha256=UfIdQLxq6ppR2zrjHZTo5kptX7PPR4Be_N2Wd7DSV4U,184
4
+ agent_framework_github_copilot/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ agent_framework_github_copilot-2.1.2.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
6
+ agent_framework_github_copilot-2.1.2.dist-info/WHEEL,sha256=rLJE8OLPBZbz7tqxs1yD-wA-ADa1Ec5PuYU-Y_PNWDI,81
7
+ agent_framework_github_copilot-2.1.2.dist-info/METADATA,sha256=46BWYaJ6ft0HBuGS5hugr2zIf4seln1LGJeA2PPMhmY,8477
8
+ agent_framework_github_copilot-2.1.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: flit 4.1.0
2
+ Generator: flit 4.2.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,8 +0,0 @@
1
- agent_framework_github_copilot/__init__.py,sha256=A8xc4tEW9aFASMSwBmiWIn6MWnOU1rXGyN3HMzEOZxw,464
2
- agent_framework_github_copilot/_agent.py,sha256=i4OgdmhEf6WycMOEh_lMWnT79O5Ay_9OKHZc6LEMf_Q,84842
3
- agent_framework_github_copilot/_feature_usage.py,sha256=UfIdQLxq6ppR2zrjHZTo5kptX7PPR4Be_N2Wd7DSV4U,184
4
- agent_framework_github_copilot/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- agent_framework_github_copilot-2.1.0.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
6
- agent_framework_github_copilot-2.1.0.dist-info/WHEEL,sha256=lqN8jXt_QloFOGcx9kStvWbryhfEXoI2F2z-YURm350,81
7
- agent_framework_github_copilot-2.1.0.dist-info/METADATA,sha256=WU9h1XvkhOYal8NzLWw87-yP5iFLrDtK1qf54rzfsVU,6547
8
- agent_framework_github_copilot-2.1.0.dist-info/RECORD,,