* Python: scope under-specified approve-for-session permission decisions
PermissionDecisionApproveForSession carries an optional `approval` (tool
prompts) and an optional `domain` (URL prompts), so it can be constructed
with neither. A bare PermissionDecisionApproveForSession() serializes to
{"kind": "approve-for-session"}, which the Copilot CLI cannot interpret: it
dereferences the absent approval and crashes the CLI process with "Cannot
read properties of undefined (reading 'commandIdentifiers')", taking the
whole run down rather than failing a single tool call.
Wrap the resolved permission handler so such decisions are scoped using the
request that triggered them: shell prompts become an approval for that
prompt's command identifiers, MCP prompts an approval for that server and
tool, URL prompts an approval for that URL's domain, and so on.
The decision is only ever narrowed, never widened. When the prompt reports
can_offer_session_approval=False, or the request kind has no session-scoped
approval (such as a hook prompt), the decision is downgraded to a single-use
approval and a warning is logged. Decisions that already specify a scope are
forwarded unchanged, and handler exceptions still propagate so the SDK's
deny-on-error behavior is preserved.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b45752e-b602-4117-8304-3c8a8b877e3e
* Fix test-suite type-checker errors for permission-decision normalizer
The permission-handler wrapper returned PermissionHandlerType (the sync-or-async
union), so awaiting its result in tests was rejected by the stricter CI type
checkers (pyrefly, ty, zuban). Give the wrapper a dedicated
AsyncPermissionHandlerType return type, and narrow the awaited result with an
isinstance assert before accessing its scope in the async-handler test.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b45752e-b602-4117-8304-3c8a8b877e3e
* Add regression tests for extension permission approval normalization
Cover the two previously-untested branches of _derive_session_approval:
extension-management preserves the request operation, and
extension-permission-access preserves the extension name. Both assert the
serialized approval payload as well.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b45752e-b602-4117-8304-3c8a8b877e3e
* Scope URL session approvals only for parser-unambiguous URLs
The URL branch derived the persisted domain with Python's urlparse, but the
Copilot CLI parses URLs with WHATWG semantics. The two disagree on crafted
authorities -- e.g. a backslash before the '@' in
'https://example.com<backslash>@evil.com' resolves to example.com under the CLI
but evil.com under urlparse -- so trusting urlparse could persist a session-wide
approval for an unrelated, attacker-chosen domain, widening authorization.
Add _derive_url_session_domain, which returns a domain only when the URL
contains none of the characters WHATWG and urlparse handle differently
(backslash, tab, newline, carriage return); any ambiguity (or a URL with no
host) narrows the decision to a single-use PermissionDecisionApproveOnce.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b45752e-b602-4117-8304-3c8a8b877e3e
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1b45752e-b602-4117-8304-3c8a8b877e3e
Get Started with Microsoft Agent Framework GitHub Copilot
Please install this package via pip:
pip install agent-framework-github-copilot
GitHub Copilot Agent
The GitHub Copilot agent enables integration with GitHub Copilot, allowing you to interact with Copilot's agentic capabilities through the Agent Framework.
Tool approval (approval_mode="always_require")
The GitHub Copilot SDK owns the tool-calling loop for this provider, so approval for custom function tools is enforced through the SDK's native pre-execution hook rather than the standard Agent Framework approval round-trip.
When you register a FunctionTool declared with approval_mode="always_require" and you
do not supply your own on_pre_tool_use hook, GitHubCopilotAgent installs a default
on_pre_tool_use hook that returns "ask" for that tool and defers (None) for all other
tools. The "ask" decision routes to your on_permission_request handler, where you
approve or deny the call:
from agent_framework import tool
from agent_framework.github import GitHubCopilotAgent, GitHubCopilotOptions
from copilot.session import PermissionHandler
@tool(approval_mode="always_require")
def delete_file(path: str) -> str:
"""Delete a file."""
...
agent = GitHubCopilotAgent(
tools=[delete_file],
# The "ask" decision is routed here; approve or deny the call.
default_options=GitHubCopilotOptions(on_permission_request=PermissionHandler.approve_all),
)
⚠️ If you provide your own
on_pre_tool_usehook, it takes precedence and the agent does not install its default approval hook. In that case you are fully responsible for enforcing approval — including for anyapproval_mode="always_require"tool (e.g. by returning a"deny"or"ask"decision). The agent logs a warning naming any approval-required tool that your hook must handle.Note: with the default (deny-all) permission handler, an
always_requiretool is denied unless you wire an approvingon_permission_request.
Approving for the rest of the session
PermissionDecisionApproveForSession scopes its approval with either an approval (tool
prompts) or a domain (URL prompts). Both are optional, so a bare
PermissionDecisionApproveForSession() carries no scope at all and the Copilot CLI cannot
interpret it.
GitHubCopilotAgent therefore scopes such a decision automatically, using the request that
triggered it — a shell prompt becomes an approval for that prompt's command identifiers, an
MCP prompt an approval for that server and tool, a URL prompt an approval for that URL's
domain, and so on:
from copilot.generated.rpc import PermissionDecisionApproveForSession
def on_permission_request(request, invocation):
# Scoped to `request` automatically; approves that kind of call for the whole session.
return PermissionDecisionApproveForSession()
The decision is only ever narrowed, never widened. When the prompt reports that it cannot
offer session-scoped approval (can_offer_session_approval=False), or the request kind has
no session-scoped approval at all (such as a hook prompt), the decision is downgraded to a
single-use approval and a warning is logged. Pass an explicit approval= or domain= when
you want to approve something other than the request being handled — decisions that already
specify a scope are forwarded unchanged.
Deprecated: on_function_approval
The on_function_approval callback is deprecated. It still works (and is still enforced
inside the tool handler for backward compatibility), but it emits a DeprecationWarning and
will be removed in a future version. Migrate to the on_pre_tool_use + on_permission_request
model described above. When on_function_approval is set, it gates always_require tools and
the default ask-hook is not installed. It is mutually exclusive with on_pre_tool_use —
setting both (whether at construction or per run) raises ValueError.