Skip to content

Commit b7e55a5

Browse files
wbrezaCopilot
andauthored
chore: upgrade Copilot SDK to v0.3.0 and CLI to v1.0.36-0 (#8114)
* Migrate event system to Copilot SDK v0.3.0 typed events Update all event handler code and tests to use the new SDK v0.3.0 event type constants (SessionEventType* prefix) and typed event data structs (replacing the flat copilot.Data struct with per-event types like AssistantMessageData, AssistantUsageData, etc.). Key changes: - Rename event constants: copilot.AssistantMessage -> copilot.SessionEventTypeAssistantMessage, etc. - Replace event.Data.Field access with type assertions: event.Data.(*copilot.XxxData) - Remove AssistantStreamingDelta case (SDK v0.3.0 only has TotalResponseSizeBytes, no phase/deltaContent) - Update SessionShutdown handler: TotalPremiumRequests is now a plain float64 - Update ToolExecutionComplete error handling: simplified Error struct - Guard Model assignment against empty string (now plain string, not *string) - Update PermissionRequestKind constants in types_test.go - Remove unused derefStr helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: upgrade copilot SDK to v0.3.0 and CLI to v1.0.36-0 Upgrade github.com/github/copilot-sdk/go from v0.1.32 to v0.3.0 and update the bundled CLI version from 1.0.2 to 1.0.36-0. Breaking changes addressed: - MCPServerConfig changed from map[string]any to a typed interface with MCPStdioServerConfig and MCPHTTPServerConfig structs - PermissionRequestKind constants renamed (e.g. MCP -> PermissionRequestKindMcp) - PermissionRequestResult Kind field now uses typed constants - CLI version regex in tests updated to allow prerelease suffixes This addresses issue #8108 where azd init Copilot Preview silently falls back from claude-opus-4.7 to claude-opus-4.6 due to the older SDK/CLI version not fully supporting newer models. Fixes #8108 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chore: apply preflight auto-fixes (formatting, spelling) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: address PR review feedback - Require type assertion success in E2E test to prevent silent pass - Broaden CLI version regex to support full SemVer prerelease identifiers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: use Rejected result kind for explicit denials Update permission result kind mappings: - User skip (ErrToolExecutionSkipped): UserNotAvailable -> Rejected (user was consulted and actively chose to skip) - Rules deny (pre-check denial): UserNotAvailable -> Rejected (definitive policy denial, not transient unavailability) This ensures the Copilot service correctly treats these as final denials rather than retryable unavailability scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 208a518 commit b7e55a5

17 files changed

Lines changed: 449 additions & 378 deletions

cli/azd/.vscode/cspell.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,9 @@ overrides:
454454
words:
455455
- filesystems
456456
- redirections
457+
- filename: internal/agent/copilot/session_config.go
458+
words:
459+
- MCPHTTP
457460
- filename: pkg/tool/version_provider.go
458461
words:
459462
- extensionquery

cli/azd/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ require (
4444
github.com/drone/envsubst v1.0.3
4545
github.com/fatih/color v1.18.0
4646
github.com/fsnotify/fsnotify v1.9.0
47-
github.com/github/copilot-sdk/go v0.1.32
47+
github.com/github/copilot-sdk/go v0.3.0
4848
github.com/gofrs/flock v0.12.1
4949
github.com/golang-jwt/jwt/v5 v5.3.0
5050
github.com/golobby/container/v3 v3.3.2

cli/azd/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z
151151
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
152152
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
153153
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
154-
github.com/github/copilot-sdk/go v0.1.32 h1:wc9SFWwxXhJts6vyzzboPLJqcEJGnHE8rMCAY1RrUgo=
155-
github.com/github/copilot-sdk/go v0.1.32/go.mod h1:qc2iEF7hdO8kzSvbyGvrcGhuk2fzdW4xTtT0+1EH2ts=
154+
github.com/github/copilot-sdk/go v0.3.0 h1:LPMpoJzUTfrPbr/5e7s5QKvi66PMmREnbZ9kRxPe6ls=
155+
github.com/github/copilot-sdk/go v0.3.0/go.mod h1:uGWkjVYcp2DV9DgtqYihh5tEoJjNqxIFaUNnrwY4FxM=
156156
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
157157
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
158158
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=

cli/azd/internal/agent/copilot/cli.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ import (
2828
)
2929

3030
// cliVersion is the Copilot CLI version that matches the SDK version in go.mod.
31-
// SDK v0.1.32 → CLI v1.0.2 (determined by the SDK's package-lock.json).
32-
const cliVersion = "1.0.2"
31+
// SDK v0.3.0 → CLI v1.0.36-0 (determined by the SDK's package-lock.json).
32+
const cliVersion = "1.0.36-0"
3333

3434
// CopilotCLI manages the Copilot CLI binary lifecycle — download, cache, and version management.
3535
// Follows the same pattern as pkg/tools/bicep for on-demand tool installation.

cli/azd/internal/agent/copilot/cli_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestDownloadURL(t *testing.T) {
4949

5050
func TestCLIVersionPinned(t *testing.T) {
5151
require.NotEmpty(t, cliVersion)
52-
require.Regexp(t, `^\d+\.\d+\.\d+$`, cliVersion)
52+
require.Regexp(t, `^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$`, cliVersion)
5353
}
5454

5555
func TestCopilotCLI_ExternalToolInterface(t *testing.T) {

cli/azd/internal/agent/copilot/copilot_sdk_e2e_test.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,26 +106,30 @@ func TestCopilotSDK_E2E(t *testing.T) {
106106

107107
// 7. Validate response
108108
t.Logf("Received %d events total", len(events))
109-
if response != nil && response.Data.Content != nil {
110-
t.Logf("Response content: %s", *response.Data.Content)
111-
require.Contains(t, *response.Data.Content, "4",
109+
if response != nil {
110+
data, ok := response.Data.(*copilot.AssistantMessageData)
111+
require.True(t, ok, "expected response.Data to be *copilot.AssistantMessageData, got %T", response.Data)
112+
t.Logf("Response content: %s", data.Content)
113+
require.Contains(t, data.Content, "4",
112114
"expected response to contain '4'")
113115
} else {
114116
// If SendAndWait returned nil, check events for assistant message
115117
var found bool
116118
for _, e := range events {
117-
if e.Type == copilot.AssistantMessage && e.Data.Content != nil {
118-
t.Logf("Found assistant message in events: %s", *e.Data.Content)
119-
found = true
120-
break
119+
if e.Type == copilot.SessionEventTypeAssistantMessage {
120+
if data, ok := e.Data.(*copilot.AssistantMessageData); ok {
121+
t.Logf("Found assistant message in events: %s", data.Content)
122+
found = true
123+
break
124+
}
121125
}
122126
}
123127
if !found {
124128
// Log all event types for debugging
125129
for _, e := range events {
126130
detail := ""
127-
if e.Data.Content != nil {
128-
detail = fmt.Sprintf(" content=%s", truncateForLog(*e.Data.Content, 100))
131+
if data, ok := e.Data.(*copilot.AssistantMessageData); ok {
132+
detail = fmt.Sprintf(" content=%s", truncateForLog(data.Content, 100))
129133
}
130134
t.Logf(" event: type=%s%s", e.Type, detail)
131135
}

cli/azd/internal/agent/copilot/helpers_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package copilot
66
import (
77
"testing"
88

9+
copilot "github.com/github/copilot-sdk/go"
10+
911
"github.com/azure/azure-dev/cli/azd/pkg/config"
1012
"github.com/stretchr/testify/require"
1113
)
@@ -80,10 +82,9 @@ func TestGetUserMCPServers(t *testing.T) {
8082
})
8183
result := getUserMCPServers(c)
8284
require.Len(t, result, 1)
83-
require.Equal(t, "http", result["myServer"]["type"])
84-
require.Equal(
85-
t, "https://example.com", result["myServer"]["url"],
86-
)
85+
httpCfg, ok := result["myServer"].(copilot.MCPHTTPServerConfig)
86+
require.True(t, ok)
87+
require.Equal(t, "https://example.com", httpCfg.URL)
8788
})
8889

8990
t.Run("EmptyMap", func(t *testing.T) {

cli/azd/internal/agent/copilot/session_config.go

Lines changed: 80 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -146,34 +146,25 @@ func (b *SessionConfigBuilder) buildMCPServers(
146146
// convertServerConfig converts an azd mcp.ServerConfig to a copilot.MCPServerConfig.
147147
func convertServerConfig(srv *mcp.ServerConfig) copilot.MCPServerConfig {
148148
if srv.Type == "http" {
149-
return copilot.MCPServerConfig{
150-
"type": "http",
151-
"url": srv.Url,
152-
"tools": []string{"*"},
149+
return copilot.MCPHTTPServerConfig{
150+
Tools: []string{"*"},
151+
URL: srv.Url,
153152
}
154153
}
155154

156-
result := copilot.MCPServerConfig{
157-
"type": "local",
158-
"command": srv.Command,
159-
"tools": []string{"*"},
160-
}
161-
162-
if len(srv.Args) > 0 {
163-
result["args"] = srv.Args
164-
}
165-
166155
envMap := make(map[string]string)
167156
for _, e := range srv.Env {
168157
if idx := indexOf(e, '='); idx > 0 {
169158
envMap[e[:idx]] = e[idx+1:]
170159
}
171160
}
172-
if len(envMap) > 0 {
173-
result["env"] = envMap
174-
}
175161

176-
return result
162+
return copilot.MCPStdioServerConfig{
163+
Tools: []string{"*"},
164+
Command: srv.Command,
165+
Args: srv.Args,
166+
Env: envMap,
167+
}
177168
}
178169

179170
// getUserMCPServers reads user-configured MCP servers from the copilot.mcp.servers config key.
@@ -194,7 +185,7 @@ func getUserMCPServers(userConfig config.Config) map[string]copilot.MCPServerCon
194185
if err := json.Unmarshal(data, &serverConfig); err != nil {
195186
continue
196187
}
197-
result[name] = copilot.MCPServerConfig(serverConfig)
188+
result[name] = mapToMCPServerConfig(serverConfig)
198189
}
199190

200191
return result
@@ -226,6 +217,65 @@ func indexOf(s string, c byte) int {
226217
return -1
227218
}
228219

220+
// mapToMCPServerConfig converts a generic map to the appropriate typed MCPServerConfig.
221+
// HTTP/SSE servers become MCPHTTPServerConfig; all others become MCPStdioServerConfig.
222+
func mapToMCPServerConfig(m map[string]any) copilot.MCPServerConfig {
223+
serverType, _ := m["type"].(string)
224+
225+
if serverType == "http" || serverType == "sse" {
226+
cfg := copilot.MCPHTTPServerConfig{}
227+
if u, ok := m["url"].(string); ok {
228+
cfg.URL = u
229+
}
230+
cfg.Tools = extractStringSlice(m["tools"])
231+
if h, ok := m["headers"].(map[string]any); ok {
232+
cfg.Headers = make(map[string]string)
233+
for k, v := range h {
234+
if s, ok := v.(string); ok {
235+
cfg.Headers[k] = s
236+
}
237+
}
238+
}
239+
return cfg
240+
}
241+
242+
cfg := copilot.MCPStdioServerConfig{}
243+
if cmd, ok := m["command"].(string); ok {
244+
cfg.Command = cmd
245+
}
246+
cfg.Args = extractStringSlice(m["args"])
247+
cfg.Tools = extractStringSlice(m["tools"])
248+
if e, ok := m["env"].(map[string]any); ok {
249+
cfg.Env = make(map[string]string)
250+
for k, v := range e {
251+
if s, ok := v.(string); ok {
252+
cfg.Env[k] = s
253+
}
254+
}
255+
}
256+
return cfg
257+
}
258+
259+
// extractStringSlice converts an any value to a string slice.
260+
func extractStringSlice(v any) []string {
261+
if v == nil {
262+
return nil
263+
}
264+
switch s := v.(type) {
265+
case []string:
266+
return s
267+
case []any:
268+
result := make([]string, 0, len(s))
269+
for _, item := range s {
270+
if str, ok := item.(string); ok {
271+
result = append(result, str)
272+
}
273+
}
274+
return result
275+
}
276+
return nil
277+
}
278+
229279
// discoverAzurePluginSkillDirs finds the skills directory from the installed
230280
// Azure plugin so skills are available in headless SDK sessions.
231281
func discoverAzurePluginSkillDirs() []string {
@@ -282,25 +332,23 @@ func loadAzurePluginMCPServers() map[string]copilot.MCPServerConfig {
282332

283333
result := make(map[string]copilot.MCPServerConfig)
284334
for name, srv := range pluginConfig.MCPServers {
285-
cfg := copilot.MCPServerConfig(srv)
286-
287-
// Normalize: ensure tools field is set to expose all tools
288-
if _, hasTools := cfg["tools"]; !hasTools {
289-
cfg["tools"] = []string{"*"}
290-
}
291-
292335
// Normalize: use "local" instead of "stdio" for local servers
293-
if t, ok := cfg["type"].(string); ok && t == "stdio" {
294-
cfg["type"] = "local"
336+
if t, ok := srv["type"].(string); ok && t == "stdio" {
337+
srv["type"] = "local"
295338
}
296339
// Default type to "local" for command-based servers
297-
if _, hasType := cfg["type"]; !hasType {
298-
if _, hasCmd := cfg["command"]; hasCmd {
299-
cfg["type"] = "local"
340+
if _, hasType := srv["type"]; !hasType {
341+
if _, hasCmd := srv["command"]; hasCmd {
342+
srv["type"] = "local"
300343
}
301344
}
302345

303-
result[name] = cfg
346+
// Normalize: ensure tools field is set to expose all tools
347+
if _, hasTools := srv["tools"]; !hasTools {
348+
srv["tools"] = []string{"*"}
349+
}
350+
351+
result[name] = mapToMCPServerConfig(srv)
304352
}
305353

306354
log.Printf("[copilot-config] Loaded %d MCP servers from Azure plugin", len(result))

cli/azd/internal/agent/copilot/session_config_test.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ package copilot
66
import (
77
"testing"
88

9+
copilot "github.com/github/copilot-sdk/go"
10+
911
"github.com/azure/azure-dev/cli/azd/internal/mcp"
1012
"github.com/azure/azure-dev/cli/azd/pkg/config"
1113
"github.com/stretchr/testify/require"
@@ -117,11 +119,14 @@ func TestSessionConfigBuilder_Build(t *testing.T) {
117119

118120
// User config overrides built-in "azd"
119121
azdServer := cfg.MCPServers["azd"]
120-
require.Equal(t, "/custom/azd", azdServer["command"])
122+
azdStdio, ok := azdServer.(copilot.MCPStdioServerConfig)
123+
require.True(t, ok)
124+
require.Equal(t, "/custom/azd", azdStdio.Command)
121125

122126
// User adds new "custom" server
123127
customServer := cfg.MCPServers["custom"]
124-
require.Equal(t, "http", customServer["type"])
128+
_, ok = customServer.(copilot.MCPHTTPServerConfig)
129+
require.True(t, ok)
125130
})
126131
}
127132

@@ -135,15 +140,13 @@ func TestConvertServerConfig(t *testing.T) {
135140
}
136141

137142
result := convertServerConfig(srv)
138-
require.Equal(t, "local", result["type"])
139-
require.Equal(t, "npx", result["command"])
140-
require.Equal(t, []string{"-y", "@azure/mcp@latest"}, result["args"])
141-
require.Equal(t, []string{"*"}, result["tools"])
142-
143-
envMap, ok := result["env"].(map[string]string)
143+
stdioResult, ok := result.(copilot.MCPStdioServerConfig)
144144
require.True(t, ok)
145-
require.Equal(t, "VALUE", envMap["KEY"])
146-
require.Equal(t, "test", envMap["OTHER"])
145+
require.Equal(t, "npx", stdioResult.Command)
146+
require.Equal(t, []string{"-y", "@azure/mcp@latest"}, stdioResult.Args)
147+
require.Equal(t, []string{"*"}, stdioResult.Tools)
148+
require.Equal(t, "VALUE", stdioResult.Env["KEY"])
149+
require.Equal(t, "test", stdioResult.Env["OTHER"])
147150
})
148151

149152
t.Run("HttpServer", func(t *testing.T) {
@@ -153,8 +156,9 @@ func TestConvertServerConfig(t *testing.T) {
153156
}
154157

155158
result := convertServerConfig(srv)
156-
require.Equal(t, "http", result["type"])
157-
require.Equal(t, "https://example.com/mcp", result["url"])
159+
httpResult, ok := result.(copilot.MCPHTTPServerConfig)
160+
require.True(t, ok)
161+
require.Equal(t, "https://example.com/mcp", httpResult.URL)
158162
})
159163
}
160164

0 commit comments

Comments
 (0)