Skip to content

Commit 2f86895

Browse files
authored
Align provider to github-copilot-sdk 1.0.0b10; release v2.3.0 (#69)
* chore(deps): bump github-copilot-sdk to 1.0.0b10; release v2.3.0 Pin the SDK at 1.0.0b10 and bump the provider to 2.3.0. Behavior changes, surface deltas, and rollback guidance land with the contract, test, and MIGRATION updates later in this series. Declare `packaging` as an explicit runtime dependency. The PEP 440 parser is imported at provider load time by the SDK floor check; pip's vendored copy at `pip._vendor.packaging` is not visible as a top-level import, so a minimal venv without an explicit dep can fail at provider import. The `>=20` lower bound covers the stable `Version` / `InvalidVersion` API surface used here. Quality: ruff: 0 errors | pyright: 0 errors | pytest (linux/py3.14): 1337 passed | pytest (win32/py3.13): 1335 passed | total coverage: 98% * feat(sdk-boundary): wire SDK 1.0.0b10 surface end-to-end CopilotClient construction SubprocessConfig was removed from the SDK at b7 and remains absent in b10 (verified in b10 `copilot/client.py` and `copilot/__init__.py`). Adopt the direct keyword-only constructor: `CopilotClient(base_directory=..., env=..., log_level=..., mode=..., github_token=...)`. Both the token and no-token branches share a single kwargs dict; only `github_token` differs. `mode` is passed explicitly ("copilot-cli") rather than relying on the SDK default, so a future SDK default change cannot silently move us off it. Permission rejection Replace `PermissionRequestResult(kind="reject")` with `PermissionDecisionReject()` (b10 `generated/rpc.py:10054`). In b10 `PermissionRequestResult` is a `PermissionDecision | PermissionNoResult` type alias (`session.py:275`), not a constructor. The new dataclass has `kind: ClassVar[str] = "reject"` and `feedback: str | None = None` whose serializer omits `feedback` when None, preserving the silent- reject wire shape used previously. MinimalMode (sdk-boundary:MinimalMode:MUST:7-14) Pin 8 SDK session-capability kwargs as defense-in-depth so the SDK side of the boundary cannot acquire context-construction, telemetry, embedding, or host-git authority. MUST:7-13 are new in b10 (`client.py:1582-1605`); MUST:14 (`enable_session_telemetry`) was a pre-existing b9 kwarg now consolidated under MinimalMode. Each has an empty-mode default helper at b10 `_mode.py:185-258` that fires only when `mode == "empty"`; under `mode="copilot-cli"` the explicit pins are the wire shape — leaving any of them None lets the bundled CLI defaults apply. Env scrubbing Extract `scrub_sdk_env()` so production wiring and the live-test fixture share one implementation. b10 keeps `env=` as a REPLACE (`client.py:3182-3185`) and injects `COPILOT_HOME` from `base_directory` afterwards (`client.py:3199`), so ambient `COPILOT_HOME` / `COPILOT_CLI_PATH` must be stripped before the call. Prewarm task cancellation Extract `_cancel_prewarm_task()` and call it from both the success- path cleanup closure and the `mount()` exception handler. Without the failure-path call, a prewarm task could outlive the client release triggered by a mount failure (sdk-protection:Subprocess:MUST:5). SDK floor check Replace hand-rolled integer-tuple parsing with `packaging.Version` comparison against `Version("1.0.0b10")`. The floor is the symbol- availability minimum (`>=1.0.0b10`); the exact pin (`==1.0.0b10`) is enforced separately by the test-suite version gate. Unparseable inputs map to a sentinel below every real release (fail closed). Event router Add 6 b9 SDK event types to the no-emit allowlist: `session.autopilot_objective_changed`, `session.permissions_changed`, `session.custom_notification`, `session.canvas.opened`, `session.canvas.registry_changed`, `mcp_app.tool_call_complete`. Each has no kernel-domain mapping; rationale documented inline. Version: bump `__version__` literal to "2.3.0" to match pyproject.toml. Quality: ruff: 0 errors | pyright: 0 errors | pytest (linux/py3.14): 1337 passed | pytest (win32/py3.13): 1335 passed | total coverage: 98% * docs(contracts): sdk-boundary v1.9 + filesystem-layout v1.2 (SDK b10) Align both contracts with the b10 SDK surface wired in this series. sdk-boundary.md (1.6 -> 1.9) - MinimalMode extended from MUST:1-6 to MUST:1-14 (7 new b10 create_session kwargs + telemetry consolidated under one place). - SDKSurface:MUST:1 rewritten: PermissionRequestResult is a type alias; rejection is PermissionDecisionReject(). - SDKSurface:MUST:6 updated to the b10 send() shape (5 keyword-only incl. display_prompt). - SDKSurface:MUST:8 added: CopilotClient.__init__ keyword-only pin. - ImportQuarantine:MUST:7 added: copilot.generated.* carve-out confined to sdk_adapter/_imports.py with TODO/HACK markers. filesystem-layout.md (1.0 -> 1.2) - Wiring MUST:1/2/5 rewritten from SubprocessConfig(copilot_home=...) to CopilotClient(base_directory=..., env=...). - env= REPLACE semantics make the ambient COPILOT_HOME / COPILOT_CLI_PATH scrub mandatory. - Test-anchor table aligned to current fixtures. Contracts only; no production code change. Quality: ruff: 0 errors | pyright: 0 errors | pytest (linux/py3.14): 1337 passed | pytest (win32/py3.13): 1335 passed | total coverage: 98% * chore(stubs): align typings/copilot/*.pyi with SDK 1.0.0b10 Stubs are pyright-only and have no runtime effect; the membrane stays narrow (opaque `Any` for the long tail of re-exports the provider does not construct), with precise typing reserved for symbols the provider actually imports. client.pyi - Remove SubprocessConfig (deleted from the SDK at b7). - Add CopilotClient with keyword-only __init__ matching b10 client.py:1073 (13 keyword params; positional construction is a type error). - Add ModelInfo / ModelPolicy / ModelBilling with the exact required vs defaulted field split from b10 client.py:645 / :670 / :691, so call-site shape errors fail type-check. - Add LogLevel = Literal[...] matching b10 client.py:110. session.pyi - PermissionRequestResult is now a TypeAlias of `PermissionDecision | PermissionNoResult` (b10 session.py:275), not a dataclass; permission denial is constructed via `PermissionDecisionReject()` from copilot.generated.rpc. - Add PermissionNoResult dataclass sentinel (b10 session.py:257). - Move CopilotSession declaration here (canonical home is copilot.session per b10 session.py:1066); root re-export retained in __init__.pyi. - CopilotSession.send / send_and_wait extended to the b10 keyword- only signature: attachments, mode, agent_mode, request_headers, display_prompt (display_prompt added in b10). types.pyi - Remove SubprocessConfig. - Tighten BlobAttachment: mimeType required, displayName NotRequired (b10 session.py:149). - Tighten ModelPolicy: state and terms required, no defaults (b10 client.py:645). __init__.pyi - Re-exports aligned with b10 __init__.py __all__ (~115 symbols). - LargeToolOutputConfig and ReasoningSummary added (new in b10). Quality: ruff: 0 errors | pyright: 0 errors | pytest (linux/py3.14): 1337 passed | pytest (win32/py3.13): 1335 passed | total coverage: 98% * test(sdk-boundary): align fixtures and assertions to SDK 1.0.0b10 Pins the test surface to the b10 wire shape so the suite fails loud on SDK regressions instead of drifting silently. - New `tests/_sdk_version_gate.py` reads the canonical pinned version and `importlib.metadata.version("github-copilot-sdk")`. Fails (never skips) on mismatch; imported unconditionally from `conftest.py` so any environment drift surfaces at collection time. - `conftest.py` drops the legacy `_default_permission_handler` and constructs `sdk_client` via the b10 keyword-only `CopilotClient(...)` signature with `scrub_sdk_env(...)`, matching production wiring. - `tests/fixtures/sdk_mocks.py` extends `MockSDKSession.send` (and the error variant) with the b10 `agent_mode` and `display_prompt` kwargs. - New boundary contracts (`test_sdk_boundary_contract.py`) pin the eight MinimalMode kwargs end-to-end (MUST:7-14) plus a superset closure to catch future SDK additions. - `test_sdk_boundary_quarantine.py` re-binds the `_imports` package attribute after `sys.modules` patch contexts to prevent mock leakage into subsequent tests that resolve `from . import _imports`. - `test_filesystem_acceptance_live.py` adds a `_real_sdk_mode` context manager that reloads `_imports` + `client` and re-binds `CopilotClientWrapper` on the three module roots so class identity holds across mixed mock/live test ordering. - `test_filesystem_subprocess_env.py` drops the removed `SubprocessConfig` helper and pins the b10 5-kwarg `CopilotClient` constructor via a `FakeCopilotClient` capture fixture. - `test_sdk_assumptions.py` pins keyword-only construction, zero-arg `PermissionDecisionReject` with `ClassVar` discriminator, the `PermissionRequestResult` alias-not-constructor shape, and the `CopilotSession.send` five-keyword-only signature. - `test_permissions.py` converts a POSIX-only chmod test from a conditional skip to a runtime fail on `win32` (gate policy: tests must run or fail, never skip). - Strengthens five weak assertions in files already in scope: `is not None` placeholders replaced with isinstance pins (`copilot.client.ModelBilling`, `copilot._jsonrpc.JsonRpcError`), direct attribute access (`handle.session_id`), and a `KeyError` raise on missing `reasoning_effort` parameter. Each assertion now names the exact SDK contract that breaks if it regresses. No production wire-shape changes. SDK pin verified end-to-end via the canonical version gate. Quality: ruff: 0 errors | pyright: 0 errors | pytest (linux/py3.14): 1337 passed | pytest (win32/py3.13): 1335 passed | total coverage: 98% * docs: v2.3.0 migration guide, README SDK refs, lock metadata refresh - `MIGRATION.md`: new v2.2.x → v2.3.0 section. Documents the SDK pin bump (`1.0.0b10`), the removal of `COPILOT_CLI_PATH` / `COPILOT_HOME` forwarding to the SDK runtime, and the SDK-side removal of `copilot.SubprocessConfig` (gone since b7). Each item names the new behavior, the migration path, and a precise rollback target (provider `<=2.2.x` with `github-copilot-sdk==1.0.0b9`). - `README.md`: bumps the SDK reference in Dependencies and the model table caption to `1.0.0b10` and refreshes the public model snapshot. The runtime list is still resolved via `amplifier provider models github-copilot` against the user's plan; the table is documentation only. - `uv.lock`: regenerated to reflect the `packaging>=20` requirement shipped in commit 1 (`e63638d`). No transitive dependency changes. Quality: ruff: 0 errors | pyright: 0 errors | pytest (linux/py3.14): 1337 passed | pytest (win32/py3.13): 1335 passed | total coverage: 98% * fix(sdk-boundary): pin mcp_oauth_token_storage in MinimalMode Pins the 9th SDK mode-gated default (was 8/9). Under mode="copilot-cli" the default helper returns None, so without this pin the bundled-CLI runtime default governs OAuth token storage on the wire -- the same gap the other 8 pins close. It was harmless only because mcp_servers={} forecloses the OAuth flow, leaving the switch silently coupled to an unrelated invariant. Pinning to "in-memory" cuts that coupling and makes the deny-list complete. Also corrects three stale b10 anchors in the contract: the emit is an independent block (not gated by mcpServers); manage_schedule_enabled and coauthor_enabled are create_session kwargs that resolve to None via the post-create patch (no emit), not absent kwargs; resume-path anchor fixed. Verified the live MinimalMode payload emits mcpOAuthTokenStorage:"in-memory" via a real Amplifier CLI run; added a guard test that fails if any mode-gated default goes unpinned. Quality: ruff: 0 errors | pyright: 0 errors | pytest (linux/py3.14): 1349 passed | pytest (win32/py3.13): 1346 passed | total coverage: 98% --------- Co-authored-by: HDMowri <HDMowri@users.noreply.github.com>
1 parent c03a97a commit 2f86895

48 files changed

Lines changed: 3534 additions & 1309 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

MIGRATION.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,134 @@
1+
# Migration Guide: v2.2.x → v2.3.0
2+
3+
## Overview
4+
5+
v2.3.0 raises the `github-copilot-sdk` requirement and tightens the
6+
environment variables the provider forwards to the SDK runtime. The
7+
provider's public surface (`mount`, `GitHubCopilotProvider`, entry point,
8+
version, documented configuration env vars
9+
`AMPLIFIER_PROVIDER_GITHUB_COPILOT_HOME` / `GITHUB_TOKEN`) is unchanged.
10+
11+
User-visible changes:
12+
13+
1. SDK requirement is now `github-copilot-sdk==1.0.0b10`.
14+
2. `COPILOT_CLI_PATH` is no longer honored.
15+
3. `COPILOT_HOME` is no longer honored.
16+
4. Ambient `COPILOT_SDK_AUTH_TOKEN` is no longer forwarded to the SDK runtime.
17+
18+
---
19+
20+
## What Changed
21+
22+
### 1. SDK requirement: `github-copilot-sdk==1.0.0b10`
23+
24+
- **What:** The provider now requires `github-copilot-sdk==1.0.0b10`. On
25+
import, the provider checks the installed SDK version and raises a
26+
clear `ImportError` if the installed version is older. The bump from
27+
b9 to b10 adds 9 new `MinimalMode` session-config pins (MUST:7-15:
28+
`enable_session_store`, `enable_skills`, `enable_file_hooks`,
29+
`enable_host_git_operations`, `enable_on_demand_instruction_discovery`,
30+
`skip_embedding_retrieval`, `embedding_cache_storage`,
31+
`enable_session_telemetry`, `mcp_oauth_token_storage`) so the SDK's
32+
defense-in-depth defaults are pinned explicitly. Wire-shape change only — no
33+
provider API change.
34+
- **Behavior on upgrade:** Older SDK installs (`1.0.0b4``b9`) raise an
35+
actionable error at provider import time that names the required
36+
version and the install command.
37+
- **Replacement:** Reinstall through Amplifier
38+
(`amplifier provider install --force github-copilot`), or pin manually:
39+
`pip install 'github-copilot-sdk==1.0.0b10'`.
40+
- **Rollback:** Pin provider `==2.2.0` with `github-copilot-sdk==1.0.0b4`.
41+
The b10 pin and b4 pin cannot be mixed. (Provider `2.3.0` requires
42+
`==1.0.0b10`; pinning `2.3.x` against `b4` would fail at provider
43+
import time.)
44+
45+
### 2. `COPILOT_CLI_PATH` is no longer honored
46+
47+
- **What:** The provider no longer forwards `COPILOT_CLI_PATH` to the SDK
48+
runtime. The SDK uses its managed Copilot CLI binary for deterministic
49+
behavior.
50+
- **Behavior on upgrade:** Any ambient `COPILOT_CLI_PATH` set by the user
51+
or shell is ignored when the provider spawns the SDK runtime.
52+
- **Replacement:** None. If you depended on a custom CLI binary, pin
53+
provider `<=2.2.x` and open an issue describing the use case.
54+
- **Rollback:** Pin provider `==2.2.0` with `github-copilot-sdk==1.0.0b4`.
55+
56+
### 3. `COPILOT_HOME` is no longer honored
57+
58+
- **What:** The provider no longer forwards `COPILOT_HOME` to the SDK
59+
runtime. Session state location is controlled by the provider, not by
60+
shell env.
61+
- **Behavior on upgrade:** Any ambient `COPILOT_HOME` set by the user or
62+
shell is ignored when the provider spawns the SDK runtime.
63+
- **Replacement:** Use `AMPLIFIER_PROVIDER_GITHUB_COPILOT_HOME` to control the
64+
provider's state root. It is resolved at provider-init time and applied
65+
through the provider's path configuration.
66+
- **Rollback:** Pin provider `==2.2.0` with `github-copilot-sdk==1.0.0b4`.
67+
68+
### 4. `copilot.SubprocessConfig` is no longer imported from the SDK
69+
70+
- **What:** The provider no longer references the
71+
`copilot.SubprocessConfig` symbol. Process options that used to be passed
72+
via that wrapper (`copilot_home`, `cli_path`, `env`, `log_level`) are now
73+
passed directly as keyword arguments on the `CopilotClient(...)`
74+
constructor (`base_directory`, `env`, `log_level`, `github_token`). The
75+
SDK removed `SubprocessConfig` from its public API at b7 and it has
76+
remained absent through b10 — keeping it in the import surface would
77+
fail at import time on any supported SDK version.
78+
- **Behavior on upgrade:** None for end users — `SubprocessConfig` was
79+
never part of the provider's public API. Fork maintainers who
80+
re-exported it from provider internals (or patched it in tests) need to
81+
switch to patching `CopilotClient` directly (e.g.,
82+
`monkeypatch.setattr(client_mod, "CopilotClient", FakeCopilotClient)`).
83+
- **Replacement:** Construct `CopilotClient(base_directory=..., env=...,
84+
log_level=..., mode="copilot-cli", github_token=...)` directly. The
85+
`mode="copilot-cli"` argument is required — leaving it unset falls
86+
through to the SDK's default mode, which does not match the provider's
87+
wiring invariants. See
88+
`sdk_adapter/client.py::_ensure_client_initialized` for the canonical
89+
pattern.
90+
- **Rollback:** Not applicable — the symbol was removed upstream.
91+
92+
### 5. Ambient `COPILOT_SDK_AUTH_TOKEN` is no longer forwarded to the SDK
93+
94+
- **What:** The provider scrubs `COPILOT_SDK_AUTH_TOKEN` from the env
95+
handed to the spawned SDK subprocess. The SDK uses this variable as the
96+
transport for the GitHub token it injects from the `github_token`
97+
constructor argument (via `--auth-token-env`); the SDK only writes it
98+
into the subprocess env when `github_token` is truthy. Without scrubbing,
99+
an ambient parent-shell value would survive into the spawned process on
100+
the no-token branch and authenticate against a credential the provider
101+
never resolved.
102+
- **Behavior on upgrade:** When the provider runs without a resolved
103+
token (`GITHUB_TOKEN`, `COPILOT_AGENT_TOKEN`, `COPILOT_GITHUB_TOKEN`,
104+
and `GH_TOKEN` all unset), the SDK subprocess no longer inherits an
105+
ambient `COPILOT_SDK_AUTH_TOKEN`. The provider still fails closed on
106+
the no-token path with the usual `ProviderUnavailableError`. When the
107+
provider resolves a token through its documented variables, the SDK
108+
re-injects `COPILOT_SDK_AUTH_TOKEN` from that resolved value, so
109+
authenticated runs are unchanged.
110+
- **Replacement:** Set one of the documented token variables
111+
(`GITHUB_TOKEN`, `COPILOT_AGENT_TOKEN`, `COPILOT_GITHUB_TOKEN`, or
112+
`GH_TOKEN`) so the provider resolves the token explicitly. The SDK
113+
will then inject `COPILOT_SDK_AUTH_TOKEN` into the subprocess on the
114+
provider's behalf.
115+
- **Rollback:** Pin provider `==2.2.0` with `github-copilot-sdk==1.0.0b4`.
116+
117+
---
118+
119+
## When
120+
121+
Provider version `2.3.0`.
122+
123+
---
124+
125+
## Rollback
126+
127+
If the new SDK or env-var behavior breaks your workflow, pin provider
128+
`==2.2.0` with `github-copilot-sdk==1.0.0b4`. The two cannot be mixed.
129+
130+
---
131+
1132
# Migration Guide: v1.0.x → v2.0.0
2133

3134
## Overview

README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,27 +127,32 @@ amplifier provider models github-copilot
127127

128128
## Supported Models
129129

130-
Models are discovered dynamically from the SDK at runtime — the list reflects your GitHub Copilot plan. The tables below show the current set as of SDK 1.0.0b4; run `amplifier provider models github-copilot` for the live list.
130+
Models are discovered dynamically from the SDK at runtime — the list reflects your GitHub Copilot plan. The tables below show the current public set as of SDK 1.0.0b10; run `amplifier provider models github-copilot` for the live list.
131+
132+
**Routing:**
133+
134+
| Model ID | Context | Max Output | Capabilities |
135+
| --- | --- | --- | --- |
136+
| `auto` | 128k | 16k | streaming, tools |
131137

132138
**Anthropic:**
133139

134140
| Model ID | Context | Max Output | Capabilities |
135141
| --- | --- | --- | --- |
142+
| `claude-opus-4.8` | 200k | 32k | streaming, tools, vision, thinking |
136143
| `claude-opus-4.7` | 200k | 32k | streaming, tools, vision, thinking |
144+
| `claude-opus-4.6` | 200k | 32k | streaming, tools, vision, thinking |
145+
| `claude-opus-4.5` | 200k | 32k | streaming, tools, vision |
137146
| `claude-sonnet-4.6` | 200k | 32k | streaming, tools, vision, thinking |
138147
| `claude-sonnet-4.5` | 200k | 32k | streaming, tools, vision |
139148
| `claude-haiku-4.5` | 200k | 64k | streaming, tools, vision |
140-
| `claude-opus-4.6` | 200k | 32k | streaming, tools, vision, thinking |
141-
| `claude-opus-4.6-1m` | 1M | 64k | streaming, tools, vision, thinking |
142-
| `claude-opus-4.5` | 200k | 32k | streaming, tools, vision |
143-
| `claude-sonnet-4` | 216k | 88k | streaming, tools, vision |
144149

145150
**OpenAI:**
146151

147152
| Model ID | Context | Max Output | Capabilities |
148153
| --- | --- | --- | --- |
149-
| `gpt-5.5` | 400k | 128k | streaming, tools, vision, thinking |
150-
| `gpt-5.4` | 400k | 128k | streaming, tools, vision, thinking |
154+
| `gpt-5.5` | 1.05M | 128k | streaming, tools, vision, thinking |
155+
| `gpt-5.4` | 1.05M | 128k | streaming, tools, vision, thinking |
151156
| `gpt-5.3-codex` | 400k | 128k | streaming, tools, vision, thinking |
152157
| `gpt-5.2-codex` | 400k | 128k | streaming, tools, vision, thinking |
153158
| `gpt-5.2` | 400k | 128k | streaming, tools, vision, thinking |
@@ -504,7 +509,7 @@ Running `amplifier init` before authentication:
504509
## Dependencies
505510

506511
- `amplifier-core` (provided by Amplifier runtime, not installed separately)
507-
- `github-copilot-sdk==1.0.0b4`
512+
- `github-copilot-sdk==1.0.0b10`
508513
- `pyyaml>=6.0`
509514

510515
> **Note:** `github-copilot-sdk` is installed automatically when you install or initialize

amplifier_module_provider_github_copilot/__init__.py

Lines changed: 98 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""GitHub Copilot Provider for Amplifier.
22
3-
Three-Medium Architecture:
4-
- Python for mechanism (~300 lines)
5-
- YAML for policy (~200 lines)
6-
- Markdown for contracts (~400 lines)
3+
Two-Medium Architecture:
4+
- Python for mechanism AND policy (config/_*.py)
5+
- Markdown for contracts (contracts/*.md)
6+
7+
YAML is reserved exclusively for SDK-correlated tabular data
8+
(config/data/errors.yaml, events.yaml) — never for policy.
79
810
Contract: contracts/provider-protocol.md
911
"""
@@ -26,6 +28,9 @@
2628
from importlib.metadata import PackageNotFoundError as _PkgNotFoundError
2729
from importlib.metadata import version as _pkg_version
2830

31+
from packaging.version import InvalidVersion as _InvalidVersion
32+
from packaging.version import Version as _Version
33+
2934
from ._identity import PROVIDER_ID
3035

3136
# Single source of truth for pytest detection — defined in _platform.py.
@@ -39,42 +44,75 @@
3944

4045

4146
def _check_sdk_version(version_str: str) -> None:
42-
"""Raise ImportError if SDK version does not satisfy >=1.0.0b4.
43-
44-
Extracted for testability — module-level code that runs under SKIP_SDK_CHECK
45-
cannot be reached by unit tests; this function can be imported and tested
46-
directly.
47+
"""Raise ImportError if SDK version does not satisfy >=1.0.0b10.
48+
49+
The b10 floor matches what the adapter actually calls:
50+
``CopilotClient(base_directory=..., mode="copilot-cli", ...)`` plus the
51+
9 MinimalMode kwargs pinned at b10 (``enable_session_store``,
52+
``enable_skills``, ``enable_file_hooks``, ``enable_host_git_operations``,
53+
``enable_on_demand_instruction_discovery``, ``skip_embedding_retrieval``,
54+
``embedding_cache_storage``, ``enable_session_telemetry``,
55+
``mcp_oauth_token_storage``). Eight are new ``create_session`` kwargs in
56+
b10 (the seven feature toggles plus ``mcp_oauth_token_storage``; verified
57+
against SDK b10 ``client.py:1582-1605`` and absent from b9); passing any of
58+
them to b9 raises ``TypeError: unexpected keyword argument`` at the first
59+
``create_session(...)`` call. ``enable_session_telemetry`` is a pre-existing
60+
b9 kwarg consolidated under MinimalMode now.
61+
Surfacing the floor at import time gives the user the actionable
62+
reinstall message instead of a deferred ``TypeError``.
63+
64+
The floor is the symbol-availability minimum (``>=1.0.0b10``); the
65+
exact pyproject pin (``==1.0.0b10``) is enforced separately by
66+
``tests/_sdk_version_gate.py``. Earlier 1.x betas lack required
67+
MinimalMode kwargs and are rejected here at import time.
68+
69+
Extracted for testability — module-level code that runs under
70+
SKIP_SDK_CHECK cannot be reached by unit tests; this function can be
71+
imported and tested directly.
4772
4873
Contract: sdk-boundary:Membrane:MUST:5
4974
"""
50-
try:
51-
ver_parts = tuple(int(x) for x in version_str.split(".")[:2] if x.isdigit())
52-
except (ValueError, TypeError): # pragma: no cover — malformed version string
53-
ver_parts = (0, 0)
54-
if ver_parts < (1, 0):
75+
if _parse_sdk_version(version_str) < _SDK_FLOOR:
5576
raise ImportError(
56-
f"github-copilot-sdk=={version_str} is installed but ==1.0.0b4 is required. "
57-
"Upgrade with: pip install 'github-copilot-sdk==1.0.0b4' "
58-
"or reinstall the provider: amplifier provider install --force github-copilot"
77+
f"github-copilot-sdk=={version_str} is below the symbol-availability "
78+
"floor (>=1.0.0b10 required; MinimalMode MUST:7-15 kwargs added at b10). "
79+
"Pinned target: ==1.0.0b10 (pyproject.toml). "
80+
"Reinstall with: pip install 'github-copilot-sdk==1.0.0b10' "
81+
f"or: amplifier provider install --force {PROVIDER_ID}"
5982
)
6083

6184

85+
_SDK_FLOOR: _Version = _Version("1.0.0b10")
86+
_SDK_UNPARSEABLE: _Version = _Version("0.0.0a0")
87+
88+
89+
def _parse_sdk_version(version_str: str) -> _Version:
90+
"""Parse a github-copilot-sdk PyPI release identifier (PEP 440).
91+
92+
Uses ``packaging.version.Version`` for full PEP 440 coverage (pre/post/
93+
local/dev releases all ordered correctly). Unparseable input maps to a
94+
sentinel below every real release so the guard fails closed.
95+
"""
96+
try:
97+
return _Version(version_str)
98+
except _InvalidVersion:
99+
return _SDK_UNPARSEABLE
100+
101+
62102
if not _SKIP_SDK_CHECK: # pragma: no cover
63103
try:
64104
_sdk_version = _pkg_version("github-copilot-sdk")
65105
except _PkgNotFoundError as _e:
66106
# SDK required; tests only run with SDK installed
67107
raise ImportError(
68108
"Required dependency 'github-copilot-sdk' is not installed. "
69-
"Install with: pip install 'github-copilot-sdk==1.0.0b4'"
109+
"Install with: pip install 'github-copilot-sdk==1.0.0b10'"
70110
) from _e
71111
# Contract: sdk-boundary:Membrane:MUST:5 — fail at import time on wrong version.
72-
# Presence-only check passes silently for SDK 0.1.x which lacks SubprocessConfig,
73-
# causing a cryptic ConfigurationError deep in the init flow instead.
74112
_check_sdk_version(_sdk_version)
75113

76114
# E402: These imports are intentionally after SDK check - we verify SDK
77-
# installation before importing modules that depend on it (Three-Medium).
115+
# installation before importing modules that depend on it (Two-Medium Architecture).
78116
import logging # noqa: E402
79117
from collections.abc import Awaitable, Callable # noqa: E402
80118
from typing import Any, NoReturn # noqa: E402
@@ -89,7 +127,7 @@ def _check_sdk_version(version_str: str) -> None:
89127

90128
# Contract: provider-protocol:public_api:MUST:1 — must match pyproject.toml [project].version
91129
# Verified by tests/test_behaviors.py::TestPackageVersionConsistency
92-
__version__ = "2.2.0"
130+
__version__ = "2.3.0"
93131

94132
# Amplifier module metadata
95133
__amplifier_module_type__ = "provider"
@@ -192,6 +230,39 @@ async def _acquire_shared_client() -> CopilotClientWrapper:
192230
return result_client
193231

194232

233+
async def _cancel_prewarm_task() -> None:
234+
"""Cancel the in-flight prewarm task, if any, and await its termination.
235+
236+
Idempotent; safe to call from both the success-path cleanup() closure and
237+
the mount() failure handler. Contract: sdk-protection:Subprocess:MUST:5.
238+
"""
239+
global _prewarm_task
240+
task = _prewarm_task
241+
if task is None or task.done():
242+
_prewarm_task = None
243+
return
244+
task.cancel()
245+
try:
246+
await task
247+
except asyncio.CancelledError:
248+
pass
249+
except Exception:
250+
# _prewarm() at __init__.py:374-386 catches and logs Exception itself
251+
# (with redaction). By the time the task resolves here only
252+
# CancelledError or a re-raised non-Exception should surface; still,
253+
# log at DEBUG (not WARN) so a future regression that removes the
254+
# inner handler leaves a traceable signal without competing with
255+
# the original mount-failure WARN being handled by the caller.
256+
# Contract: sdk-protection:Subprocess:MUST:5.
257+
# Pinned by: tests/test_client_lifecycle.py::TestPrewarmSubprocess::
258+
# test_cancel_prewarm_task_suppresses_inner_exception_branch.
259+
logging.getLogger(__name__).debug(
260+
"[CLEANUP] cancel-site suppressed non-CancelledError from prewarm task",
261+
exc_info=True,
262+
)
263+
_prewarm_task = None
264+
265+
195266
async def _release_shared_client() -> None:
196267
"""Release a reference to the shared client, closing when count reaches 0.
197268
@@ -350,15 +421,7 @@ async def _prewarm() -> None:
350421

351422
async def cleanup() -> None:
352423
# Contract: sdk-protection:Subprocess:MUST:5 — Cancel prewarm task
353-
global _prewarm_task
354-
if _prewarm_task is not None and not _prewarm_task.done(): # pragma: no cover
355-
# Prewarm cancelled during shutdown — unlikely in tests
356-
_prewarm_task.cancel()
357-
try:
358-
await _prewarm_task
359-
except asyncio.CancelledError:
360-
pass # Expected
361-
_prewarm_task = None
424+
await _cancel_prewarm_task()
362425

363426
# Contract: streaming-contract:ProgressiveStreaming:SHOULD:3 — cancel tasks
364427
await provider.cancel_emit_tasks()
@@ -370,6 +433,10 @@ async def cleanup() -> None:
370433

371434
return cleanup
372435
except Exception as e:
436+
# Cancel prewarm before releasing the client it depends on.
437+
# Contract: sdk-protection:Subprocess:MUST:5 — leaked task would
438+
# otherwise survive against a closing client.
439+
await _cancel_prewarm_task()
373440
# Release our reference if mount fails
374441
await _release_shared_client()
375442

amplifier_module_provider_github_copilot/config/_paths.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ def _resolve_absolute(value: str, env_name: str) -> Path:
5757
expanded = Path(value).expanduser()
5858
if not expanded.is_absolute():
5959
raise ValueError(
60-
f"{env_name}={value!r} must be absolute after Path.expanduser(); "
61-
f"got {expanded!s}"
60+
f"{env_name}={value!r} must be absolute after Path.expanduser(); got {expanded!s}"
6261
)
6362
return expanded
6463

0 commit comments

Comments
 (0)