Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions python/packages/github_copilot/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,13 @@ from agent_framework.github import GitHubCopilotAgent
# or directly:
from agent_framework_github_copilot import GitHubCopilotAgent
```

## Session Option Defaults

`_build_session_kwargs` forwards options to the SDK verbatim except for a few keys that get
an explicit default. Besides `on_permission_request` (deny-all), the options listed in
`_WORKSPACE_CONFIG_DEFAULTS` — currently `enable_file_hooks` — default to `False` so a
session behaves the same way in every working directory. Callers opt in through
`default_options` or per-run options. Add to that dict rather than hard-coding a default
inline, and keep it to options the working directory controls: options that only shape
prompt context (for example `enable_host_git_operations`) are deliberately left alone.
23 changes: 23 additions & 0 deletions python/packages/github_copilot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,26 @@ model described above. When `on_function_approval` is set, it gates `always_requ
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`.

## Workspace-driven session options

`on_permission_request` and `on_pre_tool_use` gate **tool calls**. They do not cover
configuration the CLI picks up from the working directory it runs in, which is a separate
mechanism with its own switches.

So that a session behaves the same way in every checkout, `GitHubCopilotAgent` leaves the
following off by default:

| Option | Default | Effect when enabled |
| --- | --- | --- |
| `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. |

Opt in per agent or per run when your workflow needs the checkout to drive the session:

```python
agent = GitHubCopilotAgent(
default_options=GitHubCopilotOptions(enable_file_hooks=True),
)
```

Only enable these for a working directory whose contents you trust to act on the host.

Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ def _deny_all_permissions(
return PermissionDecisionUserNotAvailable()


# Session options that let the working directory's checked-in configuration influence what
# the CLI does on the host. The agent leaves them off so a session behaves the same way in
# every checkout; callers that want the checkout-driven behavior can turn each one back on
# through ``default_options`` or per-run options.
_WORKSPACE_CONFIG_DEFAULTS: dict[str, bool] = {
"enable_file_hooks": False,
}


class GitHubCopilotSettings(TypedDict, total=False):
"""GitHub Copilot model settings.

Expand Down Expand Up @@ -239,6 +248,14 @@ class GitHubCopilotOptions(TypedDict, total=False):
base_directory: str
"""Directory where the CLI stores session state, configuration, and other persistent data."""

enable_file_hooks: bool
"""Whether the CLI loads file hooks from the working directory's ``.github/hooks/``.

Defaults to ``False``: hook definitions checked into the working directory are ignored
unless you opt in, so a session behaves the same way regardless of which checkout it
runs in. Unrelated to the SDK callback hooks configured through ``on_pre_tool_use``.
"""

on_pre_tool_use: PreToolUseHandler
"""Pre-tool-use hook handler for the Copilot SDK.

Expand Down Expand Up @@ -1200,8 +1217,9 @@ def _build_session_kwargs(
``runtime_options`` which override them. Every key is forwarded verbatim to
the Copilot SDK, so any ``create_session`` parameter is supported without a
dedicated mapping here (an unknown name surfaces as a ``TypeError`` from the
SDK). A few keys are handled specially because they need a secure default
(``on_permission_request`` defaults to denying all requests) or transforming:
SDK). A few keys are handled specially because they need a specific default
(``on_permission_request`` defaults to denying all requests, and the options in
``_WORKSPACE_CONFIG_DEFAULTS`` default to off) or transforming:
``tools`` are merged with the agent's tools and converted to SDK tools, and
approval callbacks are turned into ``hooks``.

Expand Down Expand Up @@ -1230,6 +1248,10 @@ def _build_session_kwargs(
kwargs["on_permission_request"] = (
opts.get("on_permission_request") or self._permission_handler or _deny_all_permissions
)
# Workspace-driven session options stay off unless the caller opts in (either layer).
for option, value in _WORKSPACE_CONFIG_DEFAULTS.items():
if kwargs.get(option) is None:
kwargs[option] = value
kwargs["hooks"] = self._build_session_hooks(all_tools, kwargs)

# Strip agent-internal and client-level keys that are consumed here or in the
Expand Down
50 changes: 50 additions & 0 deletions python/packages/github_copilot/tests/test_github_copilot_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -1124,6 +1124,7 @@ async def test_session_resumed_for_same_session(
streaming=unittest.mock.ANY,
model=unittest.mock.ANY,
on_permission_request=unittest.mock.ANY,
enable_file_hooks=unittest.mock.ANY,
hooks=unittest.mock.ANY,
)

Expand Down Expand Up @@ -1904,6 +1905,55 @@ def runtime_hook(_input: Any, _context: Any) -> Any:
# on_pre_tool_use is still honored via the hooks parameter.
assert config["hooks"]["on_pre_tool_use"] is runtime_hook

async def test_workspace_config_options_default_to_off(
self,
mock_client: MagicMock,
) -> None:
"""Workspace-driven options are disabled unless the caller opts in."""
agent = GitHubCopilotAgent(client=mock_client)
await agent.start()

await agent._get_or_create_session(AgentSession()) # type: ignore[reportPrivateUsage]

config = mock_client.create_session.call_args.kwargs
assert config["enable_file_hooks"] is False
# Options that only shape prompt context are left untouched.
assert "enable_host_git_operations" not in config

async def test_workspace_config_options_honor_default_options(
self,
mock_client: MagicMock,
) -> None:
"""A caller opting in through default_options is not overridden."""
agent = GitHubCopilotAgent(
client=mock_client,
default_options=cast(Any, {"enable_file_hooks": True}),
)
await agent.start()

await agent._get_or_create_session(AgentSession()) # type: ignore[reportPrivateUsage]

config = mock_client.create_session.call_args.kwargs
assert config["enable_file_hooks"] is True

async def test_workspace_config_options_honor_runtime_options(
self,
mock_client: MagicMock,
mock_session: MagicMock,
assistant_message_event: SessionEvent,
) -> None:
"""Per-run options override the agent-level value for workspace-driven options."""
mock_session.send_and_wait.return_value = assistant_message_event

agent = GitHubCopilotAgent(
client=mock_client,
default_options=cast(Any, {"enable_file_hooks": True}),
)
await agent.run("hello", options=cast(Any, {"enable_file_hooks": False}))

config = mock_client.create_session.call_args.kwargs
assert config["enable_file_hooks"] is False


class TestGitHubCopilotAgentToolConversion:
"""Test cases for tool conversion."""
Expand Down
Loading