From f3321b23533966117387f09e740788bd09c7fc10 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 15:00:51 +0100 Subject: [PATCH 01/14] Drop github_token boilerplate from scenario fixtures Every scenario explicitly threaded GITHUB_TOKEN from the parent env into the SDK client options: - Python: github_token=os.environ.get("GITHUB_TOKEN") - TypeScript: gitHubToken: process.env.GITHUB_TOKEN - .NET: GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN") - Go: GitHubToken: os.Getenv("GITHUB_TOKEN") - Rust: opts.github_token = std::env::var("GITHUB_TOKEN").ok() This is pure boilerplate noise: the runtime CLI already reads GITHUB_TOKEN from its inherited environment when no token is provided to the SDK, so the explicit pass-through is redundant and nothing a real consumer would write. Drop the line in every scenario across all 5 languages. Exception: `auth/gh-app` keeps explicit github_token plumbing because the whole point of that scenario is to demonstrate passing an OAuth-derived token obtained at runtime. Also clean up the resulting unused imports: - Python: `import os` removed where it was only used for GITHUB_TOKEN (ruff autofixed). - Go: `"os"` import removed where it was only used for GITHUB_TOKEN (gofmt happy). - Rust: `let mut opts = ClientOptions::default(); ...; Client::start(opts)` collapsed to `Client::start(ClientOptions::default())` where opts is no longer mutated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/scenarios/auth/byok-anthropic/go/main.go | 8 ++++---- test/scenarios/auth/byok-azure/go/main.go | 8 ++++---- test/scenarios/auth/byok-ollama/go/main.go | 8 ++++---- test/scenarios/auth/byok-openai/go/main.go | 8 ++++---- test/scenarios/auth/gh-app/go/main.go | 8 ++++---- .../bundling/app-direct-server/go/main.go | 8 ++++---- .../bundling/container-proxy/go/main.go | 8 ++++---- .../bundling/fully-bundled/csharp/Program.cs | 1 - test/scenarios/bundling/fully-bundled/go/main.go | 13 +++++-------- .../bundling/fully-bundled/python/main.py | 5 +---- .../fully-bundled/typescript/src/index.ts | 1 - test/scenarios/callbacks/hooks/csharp/Program.cs | 1 - test/scenarios/callbacks/hooks/go/main.go | 5 +---- test/scenarios/callbacks/hooks/python/main.py | 5 +---- test/scenarios/callbacks/hooks/rust/src/main.rs | 4 +--- .../callbacks/hooks/typescript/src/index.ts | 1 - .../callbacks/permissions/csharp/Program.cs | 1 - test/scenarios/callbacks/permissions/go/main.go | 5 +---- .../callbacks/permissions/python/main.py | 5 +---- .../callbacks/permissions/rust/src/main.rs | 4 +--- .../permissions/typescript/src/index.ts | 1 - .../callbacks/user-input/csharp/Program.cs | 1 - test/scenarios/callbacks/user-input/go/main.go | 5 +---- .../callbacks/user-input/python/main.py | 5 +---- .../callbacks/user-input/rust/src/main.rs | 4 +--- .../callbacks/user-input/typescript/src/index.ts | 1 - test/scenarios/modes/default/csharp/Program.cs | 1 - test/scenarios/modes/default/go/main.go | 13 +++++-------- test/scenarios/modes/default/python/main.py | 5 +---- test/scenarios/modes/default/rust/src/main.rs | 4 +--- .../modes/default/typescript/src/index.ts | 1 - test/scenarios/modes/minimal/csharp/Program.cs | 1 - test/scenarios/modes/minimal/go/main.go | 13 +++++-------- test/scenarios/modes/minimal/python/main.py | 5 +---- .../modes/minimal/typescript/src/index.ts | 1 - .../prompts/attachments/csharp/Program.cs | 1 - test/scenarios/prompts/attachments/go/main.go | 4 +--- .../scenarios/prompts/attachments/python/main.py | 4 +--- .../prompts/attachments/rust/src/main.rs | 4 +--- .../prompts/attachments/typescript/src/index.ts | 1 - .../prompts/reasoning-effort/csharp/Program.cs | 1 - .../prompts/reasoning-effort/go/main.go | 5 +---- .../prompts/reasoning-effort/python/main.py | 5 +---- .../prompts/reasoning-effort/rust/src/main.rs | 4 +--- .../reasoning-effort/typescript/src/index.ts | 1 - .../prompts/system-message/csharp/Program.cs | 1 - test/scenarios/prompts/system-message/go/main.go | 13 +++++-------- .../prompts/system-message/python/main.py | 5 +---- .../prompts/system-message/rust/src/main.rs | 4 +--- .../system-message/typescript/src/index.ts | 1 - .../concurrent-sessions/csharp/Program.cs | 1 - .../sessions/concurrent-sessions/go/main.go | 5 +---- .../sessions/concurrent-sessions/python/main.py | 5 +---- .../concurrent-sessions/rust/src/main.rs | 4 +--- .../concurrent-sessions/typescript/src/index.ts | 1 - .../sessions/infinite-sessions/csharp/Program.cs | 1 - .../sessions/infinite-sessions/go/main.go | 7 ++----- .../sessions/infinite-sessions/python/main.py | 5 +---- .../sessions/infinite-sessions/rust/src/main.rs | 4 +--- .../infinite-sessions/typescript/src/index.ts | 1 - .../sessions/session-resume/csharp/Program.cs | 1 - .../scenarios/sessions/session-resume/go/main.go | 13 +++++-------- .../sessions/session-resume/python/main.py | 5 +---- .../sessions/session-resume/rust/src/main.rs | 4 +--- .../session-resume/typescript/src/index.ts | 1 - .../sessions/streaming/csharp/Program.cs | 1 - test/scenarios/sessions/streaming/go/main.go | 5 +---- test/scenarios/sessions/streaming/python/main.py | 5 +---- .../sessions/streaming/rust/src/main.rs | 4 +--- .../sessions/streaming/typescript/src/index.ts | 1 - .../tools/custom-agents/csharp/Program.cs | 1 - test/scenarios/tools/custom-agents/go/main.go | 13 +++++-------- .../scenarios/tools/custom-agents/python/main.py | 5 +---- .../tools/custom-agents/rust/src/main.rs | 4 +--- .../tools/custom-agents/typescript/src/index.ts | 1 - .../tools/mcp-servers/csharp/Program.cs | 1 - test/scenarios/tools/mcp-servers/go/main.go | 12 +++++------- test/scenarios/tools/mcp-servers/python/main.py | 4 +--- .../scenarios/tools/mcp-servers/rust/src/main.rs | 4 +--- .../tools/mcp-servers/typescript/src/index.ts | 1 - test/scenarios/tools/no-tools/csharp/Program.cs | 1 - test/scenarios/tools/no-tools/go/main.go | 13 +++++-------- test/scenarios/tools/no-tools/python/main.py | 5 +---- test/scenarios/tools/no-tools/rust/src/main.rs | 4 +--- .../tools/no-tools/typescript/src/index.ts | 1 - test/scenarios/tools/skills/csharp/Program.cs | 1 - test/scenarios/tools/skills/go/main.go | 5 +---- test/scenarios/tools/skills/python/main.py | 5 +---- test/scenarios/tools/skills/rust/src/main.rs | 4 +--- .../tools/skills/typescript/src/index.ts | 1 - .../tools/tool-filtering/csharp/Program.cs | 1 - test/scenarios/tools/tool-filtering/go/main.go | 13 +++++-------- .../tools/tool-filtering/python/main.py | 5 +---- .../tools/tool-filtering/rust/src/main.rs | 4 +--- .../tools/tool-filtering/typescript/src/index.ts | 1 - .../tools/tool-overrides/csharp/Program.cs | 1 - test/scenarios/tools/tool-overrides/go/main.go | 13 +++++-------- .../tools/tool-overrides/python/main.py | 5 +---- .../tools/tool-overrides/rust/src/main.rs | 4 +--- .../tools/tool-overrides/typescript/src/index.ts | 1 - .../tools/virtual-filesystem/csharp/Program.cs | 1 - .../tools/virtual-filesystem/go/main.go | 5 +---- .../tools/virtual-filesystem/python/main.py | 5 +---- .../virtual-filesystem/typescript/src/index.ts | 1 - test/scenarios/transport/reconnect/go/main.go | 16 ++++++++-------- test/scenarios/transport/stdio/csharp/Program.cs | 1 - test/scenarios/transport/stdio/go/main.go | 13 +++++-------- test/scenarios/transport/stdio/python/main.py | 5 +---- test/scenarios/transport/stdio/rust/src/main.rs | 4 +--- .../transport/stdio/typescript/src/index.ts | 1 - test/scenarios/transport/tcp/go/main.go | 8 ++++---- test/scenarios/transport/tcp/rust/src/main.rs | 1 - 112 files changed, 145 insertions(+), 346 deletions(-) diff --git a/test/scenarios/auth/byok-anthropic/go/main.go b/test/scenarios/auth/byok-anthropic/go/main.go index ae1ea92a0..98a67aa76 100644 --- a/test/scenarios/auth/byok-anthropic/go/main.go +++ b/test/scenarios/auth/byok-anthropic/go/main.go @@ -59,8 +59,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/auth/byok-azure/go/main.go b/test/scenarios/auth/byok-azure/go/main.go index eece7a9cd..6050929ac 100644 --- a/test/scenarios/auth/byok-azure/go/main.go +++ b/test/scenarios/auth/byok-azure/go/main.go @@ -63,8 +63,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/auth/byok-ollama/go/main.go b/test/scenarios/auth/byok-ollama/go/main.go index 8232c63dc..a507ba558 100644 --- a/test/scenarios/auth/byok-ollama/go/main.go +++ b/test/scenarios/auth/byok-ollama/go/main.go @@ -55,8 +55,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/auth/byok-openai/go/main.go b/test/scenarios/auth/byok-openai/go/main.go index 01d0b6da9..d1149a067 100644 --- a/test/scenarios/auth/byok-openai/go/main.go +++ b/test/scenarios/auth/byok-openai/go/main.go @@ -54,8 +54,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/auth/gh-app/go/main.go b/test/scenarios/auth/gh-app/go/main.go index b19d21cbd..5f774ef2c 100644 --- a/test/scenarios/auth/gh-app/go/main.go +++ b/test/scenarios/auth/gh-app/go/main.go @@ -186,8 +186,8 @@ func main() { log.Fatal(err) } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/bundling/app-direct-server/go/main.go b/test/scenarios/bundling/app-direct-server/go/main.go index acdbaab76..95da9cf68 100644 --- a/test/scenarios/bundling/app-direct-server/go/main.go +++ b/test/scenarios/bundling/app-direct-server/go/main.go @@ -41,8 +41,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/bundling/container-proxy/go/main.go b/test/scenarios/bundling/container-proxy/go/main.go index acdbaab76..95da9cf68 100644 --- a/test/scenarios/bundling/container-proxy/go/main.go +++ b/test/scenarios/bundling/container-proxy/go/main.go @@ -41,8 +41,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/bundling/fully-bundled/csharp/Program.cs b/test/scenarios/bundling/fully-bundled/csharp/Program.cs index e9dfbcccc..576ca5518 100644 --- a/test/scenarios/bundling/fully-bundled/csharp/Program.cs +++ b/test/scenarios/bundling/fully-bundled/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/bundling/fully-bundled/go/main.go b/test/scenarios/bundling/fully-bundled/go/main.go index 8fab8510d..c7716df2c 100644 --- a/test/scenarios/bundling/fully-bundled/go/main.go +++ b/test/scenarios/bundling/fully-bundled/go/main.go @@ -4,16 +4,13 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { // Go SDK auto-reads COPILOT_CLI_PATH from env - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -37,8 +34,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/bundling/fully-bundled/python/main.py b/test/scenarios/bundling/fully-bundled/python/main.py index 63c309995..6be1d4294 100644 --- a/test/scenarios/bundling/fully-bundled/python/main.py +++ b/test/scenarios/bundling/fully-bundled/python/main.py @@ -1,13 +1,10 @@ import asyncio -import os from copilot import CopilotClient async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session(model="claude-haiku-4.5") diff --git a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts index c80c1b074..38f937d59 100644 --- a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts +++ b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts @@ -3,7 +3,6 @@ import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/callbacks/hooks/csharp/Program.cs b/test/scenarios/callbacks/hooks/csharp/Program.cs index 78184df2a..3927e4c95 100644 --- a/test/scenarios/callbacks/hooks/csharp/Program.cs +++ b/test/scenarios/callbacks/hooks/csharp/Program.cs @@ -5,7 +5,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/callbacks/hooks/go/main.go b/test/scenarios/callbacks/hooks/go/main.go index 4ef48b483..a7f9650b3 100644 --- a/test/scenarios/callbacks/hooks/go/main.go +++ b/test/scenarios/callbacks/hooks/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "sync" copilot "github.com/github/copilot-sdk/go" @@ -22,9 +21,7 @@ func main() { hookLogMu.Unlock() } - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/callbacks/hooks/python/main.py b/test/scenarios/callbacks/hooks/python/main.py index 3a7b5906c..434c86509 100644 --- a/test/scenarios/callbacks/hooks/python/main.py +++ b/test/scenarios/callbacks/hooks/python/main.py @@ -1,5 +1,4 @@ import asyncio -import os from copilot import CopilotClient from copilot.generated.rpc import PermissionDecisionApproveOnce @@ -41,9 +40,7 @@ async def on_error_occurred(input_data, invocation): async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/callbacks/hooks/rust/src/main.rs b/test/scenarios/callbacks/hooks/rust/src/main.rs index c0fcc56d0..3f1cd056e 100644 --- a/test/scenarios/callbacks/hooks/rust/src/main.rs +++ b/test/scenarios/callbacks/hooks/rust/src/main.rs @@ -90,9 +90,7 @@ impl SessionHooks for HookLogger { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let hook_log = Arc::new(Mutex::new(Vec::::new())); let hooks = Arc::new(HookLogger { diff --git a/test/scenarios/callbacks/hooks/typescript/src/index.ts b/test/scenarios/callbacks/hooks/typescript/src/index.ts index 1c92c6eec..e0fdad1d6 100644 --- a/test/scenarios/callbacks/hooks/typescript/src/index.ts +++ b/test/scenarios/callbacks/hooks/typescript/src/index.ts @@ -5,7 +5,6 @@ async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/callbacks/permissions/csharp/Program.cs b/test/scenarios/callbacks/permissions/csharp/Program.cs index cf3275e56..683cfcda4 100644 --- a/test/scenarios/callbacks/permissions/csharp/Program.cs +++ b/test/scenarios/callbacks/permissions/csharp/Program.cs @@ -5,7 +5,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/callbacks/permissions/go/main.go b/test/scenarios/callbacks/permissions/go/main.go index 23715727b..1397d9ece 100644 --- a/test/scenarios/callbacks/permissions/go/main.go +++ b/test/scenarios/callbacks/permissions/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "sync" copilot "github.com/github/copilot-sdk/go" @@ -16,9 +15,7 @@ func main() { permissionLogMu sync.Mutex ) - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/callbacks/permissions/python/main.py b/test/scenarios/callbacks/permissions/python/main.py index 138d6310a..2e8e5c3e5 100644 --- a/test/scenarios/callbacks/permissions/python/main.py +++ b/test/scenarios/callbacks/permissions/python/main.py @@ -1,5 +1,4 @@ import asyncio -import os from copilot import CopilotClient from copilot.generated.rpc import PermissionDecisionApproveOnce @@ -18,9 +17,7 @@ async def auto_approve_tool(input_data, invocation): async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/callbacks/permissions/rust/src/main.rs b/test/scenarios/callbacks/permissions/rust/src/main.rs index c44b691bf..287cfd9f1 100644 --- a/test/scenarios/callbacks/permissions/rust/src/main.rs +++ b/test/scenarios/callbacks/permissions/rust/src/main.rs @@ -50,9 +50,7 @@ impl SessionHooks for AllowAllHooks { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let permission_log = Arc::new(Mutex::new(Vec::::new())); let handler = Arc::new(PermissionLogger { diff --git a/test/scenarios/callbacks/permissions/typescript/src/index.ts b/test/scenarios/callbacks/permissions/typescript/src/index.ts index a9668d0b5..04cce2713 100644 --- a/test/scenarios/callbacks/permissions/typescript/src/index.ts +++ b/test/scenarios/callbacks/permissions/typescript/src/index.ts @@ -5,7 +5,6 @@ async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/callbacks/user-input/csharp/Program.cs b/test/scenarios/callbacks/user-input/csharp/Program.cs index e9fe06968..4455622a7 100644 --- a/test/scenarios/callbacks/user-input/csharp/Program.cs +++ b/test/scenarios/callbacks/user-input/csharp/Program.cs @@ -5,7 +5,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/callbacks/user-input/go/main.go b/test/scenarios/callbacks/user-input/go/main.go index a0baf2936..a4fcdfedf 100644 --- a/test/scenarios/callbacks/user-input/go/main.go +++ b/test/scenarios/callbacks/user-input/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "sync" copilot "github.com/github/copilot-sdk/go" @@ -16,9 +15,7 @@ var ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/callbacks/user-input/python/main.py b/test/scenarios/callbacks/user-input/python/main.py index 9eff3c7cc..2d5bdb1e9 100644 --- a/test/scenarios/callbacks/user-input/python/main.py +++ b/test/scenarios/callbacks/user-input/python/main.py @@ -1,5 +1,4 @@ import asyncio -import os from copilot import CopilotClient from copilot.generated.rpc import PermissionDecisionApproveOnce @@ -21,9 +20,7 @@ async def handle_user_input(request, invocation): async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/callbacks/user-input/rust/src/main.rs b/test/scenarios/callbacks/user-input/rust/src/main.rs index 1517727e9..9f805a367 100644 --- a/test/scenarios/callbacks/user-input/rust/src/main.rs +++ b/test/scenarios/callbacks/user-input/rust/src/main.rs @@ -65,9 +65,7 @@ impl SessionHooks for AllowAllHooks { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let input_log = Arc::new(Mutex::new(Vec::::new())); let handler = Arc::new(InputResponder { diff --git a/test/scenarios/callbacks/user-input/typescript/src/index.ts b/test/scenarios/callbacks/user-input/typescript/src/index.ts index 7980c3adf..c41f9cc3a 100644 --- a/test/scenarios/callbacks/user-input/typescript/src/index.ts +++ b/test/scenarios/callbacks/user-input/typescript/src/index.ts @@ -5,7 +5,6 @@ async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/modes/default/csharp/Program.cs b/test/scenarios/modes/default/csharp/Program.cs index 23d6c63f0..41a33a379 100644 --- a/test/scenarios/modes/default/csharp/Program.cs +++ b/test/scenarios/modes/default/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/modes/default/go/main.go b/test/scenarios/modes/default/go/main.go index b0c44459f..e2b5f3634 100644 --- a/test/scenarios/modes/default/go/main.go +++ b/test/scenarios/modes/default/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -36,10 +33,10 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Printf("Response: %s\n", d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Printf("Response: %s\n", d.Content) + } + } fmt.Println("Default mode test complete") } diff --git a/test/scenarios/modes/default/python/main.py b/test/scenarios/modes/default/python/main.py index 3bb6e10a3..54d937be0 100644 --- a/test/scenarios/modes/default/python/main.py +++ b/test/scenarios/modes/default/python/main.py @@ -1,13 +1,10 @@ import asyncio -import os from copilot import CopilotClient async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session(model="claude-haiku-4.5") diff --git a/test/scenarios/modes/default/rust/src/main.rs b/test/scenarios/modes/default/rust/src/main.rs index d316c1a0a..d5d51ce8d 100644 --- a/test/scenarios/modes/default/rust/src/main.rs +++ b/test/scenarios/modes/default/rust/src/main.rs @@ -9,9 +9,7 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); diff --git a/test/scenarios/modes/default/typescript/src/index.ts b/test/scenarios/modes/default/typescript/src/index.ts index 72ae28960..937970e2c 100644 --- a/test/scenarios/modes/default/typescript/src/index.ts +++ b/test/scenarios/modes/default/typescript/src/index.ts @@ -3,7 +3,6 @@ import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/modes/minimal/csharp/Program.cs b/test/scenarios/modes/minimal/csharp/Program.cs index 70081b58d..44a0d31e2 100644 --- a/test/scenarios/modes/minimal/csharp/Program.cs +++ b/test/scenarios/modes/minimal/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/modes/minimal/go/main.go b/test/scenarios/modes/minimal/go/main.go index dc9ad0190..909938139 100644 --- a/test/scenarios/modes/minimal/go/main.go +++ b/test/scenarios/modes/minimal/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -41,10 +38,10 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Printf("Response: %s\n", d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Printf("Response: %s\n", d.Content) + } + } fmt.Println("Minimal mode test complete") } diff --git a/test/scenarios/modes/minimal/python/main.py b/test/scenarios/modes/minimal/python/main.py index 71811d377..e9455daee 100644 --- a/test/scenarios/modes/minimal/python/main.py +++ b/test/scenarios/modes/minimal/python/main.py @@ -1,13 +1,10 @@ import asyncio -import os from copilot import CopilotClient async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/modes/minimal/typescript/src/index.ts b/test/scenarios/modes/minimal/typescript/src/index.ts index 894e31798..54e9a226f 100644 --- a/test/scenarios/modes/minimal/typescript/src/index.ts +++ b/test/scenarios/modes/minimal/typescript/src/index.ts @@ -3,7 +3,6 @@ import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/prompts/attachments/csharp/Program.cs b/test/scenarios/prompts/attachments/csharp/Program.cs index 7cafcb86d..c98bf1f65 100644 --- a/test/scenarios/prompts/attachments/csharp/Program.cs +++ b/test/scenarios/prompts/attachments/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/prompts/attachments/go/main.go b/test/scenarios/prompts/attachments/go/main.go index 44c79cf6c..c29294e56 100644 --- a/test/scenarios/prompts/attachments/go/main.go +++ b/test/scenarios/prompts/attachments/go/main.go @@ -13,9 +13,7 @@ import ( const systemPrompt = `You are a helpful assistant. Answer questions about attached files concisely.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/prompts/attachments/python/main.py b/test/scenarios/prompts/attachments/python/main.py index 998770298..584d86ae9 100644 --- a/test/scenarios/prompts/attachments/python/main.py +++ b/test/scenarios/prompts/attachments/python/main.py @@ -7,9 +7,7 @@ async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/prompts/attachments/rust/src/main.rs b/test/scenarios/prompts/attachments/rust/src/main.rs index ea96d5b56..040ee5cde 100644 --- a/test/scenarios/prompts/attachments/rust/src/main.rs +++ b/test/scenarios/prompts/attachments/rust/src/main.rs @@ -13,9 +13,7 @@ const SYSTEM_PROMPT: &str = #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); diff --git a/test/scenarios/prompts/attachments/typescript/src/index.ts b/test/scenarios/prompts/attachments/typescript/src/index.ts index 4448c1dad..65f388205 100644 --- a/test/scenarios/prompts/attachments/typescript/src/index.ts +++ b/test/scenarios/prompts/attachments/typescript/src/index.ts @@ -7,7 +7,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/prompts/reasoning-effort/csharp/Program.cs b/test/scenarios/prompts/reasoning-effort/csharp/Program.cs index 2ed2ae94d..bef7fa6f5 100644 --- a/test/scenarios/prompts/reasoning-effort/csharp/Program.cs +++ b/test/scenarios/prompts/reasoning-effort/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/prompts/reasoning-effort/go/main.go b/test/scenarios/prompts/reasoning-effort/go/main.go index af5381263..5ce6a4a11 100644 --- a/test/scenarios/prompts/reasoning-effort/go/main.go +++ b/test/scenarios/prompts/reasoning-effort/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/prompts/reasoning-effort/python/main.py b/test/scenarios/prompts/reasoning-effort/python/main.py index ae4b0264f..860217d41 100644 --- a/test/scenarios/prompts/reasoning-effort/python/main.py +++ b/test/scenarios/prompts/reasoning-effort/python/main.py @@ -1,13 +1,10 @@ import asyncio -import os from copilot import CopilotClient async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/prompts/reasoning-effort/rust/src/main.rs b/test/scenarios/prompts/reasoning-effort/rust/src/main.rs index f675da5e5..74c7296a4 100644 --- a/test/scenarios/prompts/reasoning-effort/rust/src/main.rs +++ b/test/scenarios/prompts/reasoning-effort/rust/src/main.rs @@ -9,9 +9,7 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); diff --git a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts index c6d2917d8..300051f5e 100644 --- a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts +++ b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts @@ -3,7 +3,6 @@ import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/prompts/system-message/csharp/Program.cs b/test/scenarios/prompts/system-message/csharp/Program.cs index 48afdd7ba..03d0ad185 100644 --- a/test/scenarios/prompts/system-message/csharp/Program.cs +++ b/test/scenarios/prompts/system-message/csharp/Program.cs @@ -5,7 +5,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/prompts/system-message/go/main.go b/test/scenarios/prompts/system-message/go/main.go index a49d65d88..138a14e92 100644 --- a/test/scenarios/prompts/system-message/go/main.go +++ b/test/scenarios/prompts/system-message/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) @@ -12,9 +11,7 @@ import ( const piratePrompt = `You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -43,8 +40,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/prompts/system-message/python/main.py b/test/scenarios/prompts/system-message/python/main.py index 490347234..a2bc31c76 100644 --- a/test/scenarios/prompts/system-message/python/main.py +++ b/test/scenarios/prompts/system-message/python/main.py @@ -1,5 +1,4 @@ import asyncio -import os from copilot import CopilotClient @@ -7,9 +6,7 @@ async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/prompts/system-message/rust/src/main.rs b/test/scenarios/prompts/system-message/rust/src/main.rs index 7233a64b9..20893bdcc 100644 --- a/test/scenarios/prompts/system-message/rust/src/main.rs +++ b/test/scenarios/prompts/system-message/rust/src/main.rs @@ -11,9 +11,7 @@ in every response. Use nautical terms and pirate slang throughout."; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); diff --git a/test/scenarios/prompts/system-message/typescript/src/index.ts b/test/scenarios/prompts/system-message/typescript/src/index.ts index a0bb44ac8..892c0a517 100644 --- a/test/scenarios/prompts/system-message/typescript/src/index.ts +++ b/test/scenarios/prompts/system-message/typescript/src/index.ts @@ -5,7 +5,6 @@ const PIRATE_PROMPT = `You are a pirate. Always respond in pirate speak. Say 'Ar async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs b/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs index a5ba2577b..419804bd8 100644 --- a/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs +++ b/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs @@ -6,7 +6,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/sessions/concurrent-sessions/go/main.go b/test/scenarios/sessions/concurrent-sessions/go/main.go index e399fedf7..ffcc5eb6d 100644 --- a/test/scenarios/sessions/concurrent-sessions/go/main.go +++ b/test/scenarios/sessions/concurrent-sessions/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "sync" copilot "github.com/github/copilot-sdk/go" @@ -14,9 +13,7 @@ const piratePrompt = `You are a pirate. Always say Arrr!` const robotPrompt = `You are a robot. Always say BEEP BOOP!` func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/sessions/concurrent-sessions/python/main.py b/test/scenarios/sessions/concurrent-sessions/python/main.py index beee2ba06..a49ee283f 100644 --- a/test/scenarios/sessions/concurrent-sessions/python/main.py +++ b/test/scenarios/sessions/concurrent-sessions/python/main.py @@ -1,5 +1,4 @@ import asyncio -import os from copilot import CopilotClient @@ -8,9 +7,7 @@ async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session1, session2 = await asyncio.gather( diff --git a/test/scenarios/sessions/concurrent-sessions/rust/src/main.rs b/test/scenarios/sessions/concurrent-sessions/rust/src/main.rs index 7a64a6b92..48a2c7166 100644 --- a/test/scenarios/sessions/concurrent-sessions/rust/src/main.rs +++ b/test/scenarios/sessions/concurrent-sessions/rust/src/main.rs @@ -24,9 +24,7 @@ fn make_config(system: &str) -> SessionConfig { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let session1 = client.create_session(make_config(PIRATE_PROMPT)).await?; let session2 = client.create_session(make_config(ROBOT_PROMPT)).await?; diff --git a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts index 81f671e91..39892b0ca 100644 --- a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts @@ -6,7 +6,6 @@ const ROBOT_PROMPT = `You are a robot. Always say BEEP BOOP!`; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/sessions/infinite-sessions/csharp/Program.cs b/test/scenarios/sessions/infinite-sessions/csharp/Program.cs index 2619f25b4..4b81a99e2 100644 --- a/test/scenarios/sessions/infinite-sessions/csharp/Program.cs +++ b/test/scenarios/sessions/infinite-sessions/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/sessions/infinite-sessions/go/main.go b/test/scenarios/sessions/infinite-sessions/go/main.go index 29871eacc..134239b0a 100644 --- a/test/scenarios/sessions/infinite-sessions/go/main.go +++ b/test/scenarios/sessions/infinite-sessions/go/main.go @@ -4,18 +4,15 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) -func boolPtr(b bool) *bool { return &b } +func boolPtr(b bool) *bool { return &b } func float64Ptr(f float64) *float64 { return &f } func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/sessions/infinite-sessions/python/main.py b/test/scenarios/sessions/infinite-sessions/python/main.py index a41b2b4fa..47dab5bb8 100644 --- a/test/scenarios/sessions/infinite-sessions/python/main.py +++ b/test/scenarios/sessions/infinite-sessions/python/main.py @@ -1,13 +1,10 @@ import asyncio -import os from copilot import CopilotClient async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/sessions/infinite-sessions/rust/src/main.rs b/test/scenarios/sessions/infinite-sessions/rust/src/main.rs index 2ccb1d786..2882254e2 100644 --- a/test/scenarios/sessions/infinite-sessions/rust/src/main.rs +++ b/test/scenarios/sessions/infinite-sessions/rust/src/main.rs @@ -9,9 +9,7 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); diff --git a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts index e2a8c5fdb..da573b561 100644 --- a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts @@ -3,7 +3,6 @@ import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/sessions/session-resume/csharp/Program.cs b/test/scenarios/sessions/session-resume/csharp/Program.cs index a1bd015bd..327628755 100644 --- a/test/scenarios/sessions/session-resume/csharp/Program.cs +++ b/test/scenarios/sessions/session-resume/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/sessions/session-resume/go/main.go b/test/scenarios/sessions/session-resume/go/main.go index 330fb6852..aca5cb16d 100644 --- a/test/scenarios/sessions/session-resume/go/main.go +++ b/test/scenarios/sessions/session-resume/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -60,8 +57,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/sessions/session-resume/python/main.py b/test/scenarios/sessions/session-resume/python/main.py index 6d7ae02a2..7dab1b309 100644 --- a/test/scenarios/sessions/session-resume/python/main.py +++ b/test/scenarios/sessions/session-resume/python/main.py @@ -1,13 +1,10 @@ import asyncio -import os from copilot import CopilotClient async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: # 1. Create a session diff --git a/test/scenarios/sessions/session-resume/rust/src/main.rs b/test/scenarios/sessions/session-resume/rust/src/main.rs index b6e6fbf8b..2f1815a78 100644 --- a/test/scenarios/sessions/session-resume/rust/src/main.rs +++ b/test/scenarios/sessions/session-resume/rust/src/main.rs @@ -9,9 +9,7 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); diff --git a/test/scenarios/sessions/session-resume/typescript/src/index.ts b/test/scenarios/sessions/session-resume/typescript/src/index.ts index c9ba3b3d5..25b87c628 100644 --- a/test/scenarios/sessions/session-resume/typescript/src/index.ts +++ b/test/scenarios/sessions/session-resume/typescript/src/index.ts @@ -3,7 +3,6 @@ import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/sessions/streaming/csharp/Program.cs b/test/scenarios/sessions/streaming/csharp/Program.cs index 518d4b227..8a0300840 100644 --- a/test/scenarios/sessions/streaming/csharp/Program.cs +++ b/test/scenarios/sessions/streaming/csharp/Program.cs @@ -2,7 +2,6 @@ var options = new CopilotClientOptions { - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }; var cliPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); diff --git a/test/scenarios/sessions/streaming/go/main.go b/test/scenarios/sessions/streaming/go/main.go index 8a1c78efa..edf70c801 100644 --- a/test/scenarios/sessions/streaming/go/main.go +++ b/test/scenarios/sessions/streaming/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/sessions/streaming/python/main.py b/test/scenarios/sessions/streaming/python/main.py index 6c1c19f7b..8def95f1f 100644 --- a/test/scenarios/sessions/streaming/python/main.py +++ b/test/scenarios/sessions/streaming/python/main.py @@ -1,13 +1,10 @@ import asyncio -import os from copilot import CopilotClient async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session(model="claude-haiku-4.5", streaming=True) diff --git a/test/scenarios/sessions/streaming/rust/src/main.rs b/test/scenarios/sessions/streaming/rust/src/main.rs index d4201ef06..da0cba5e3 100644 --- a/test/scenarios/sessions/streaming/rust/src/main.rs +++ b/test/scenarios/sessions/streaming/rust/src/main.rs @@ -10,9 +10,7 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let chunks = Arc::new(AtomicUsize::new(0)); diff --git a/test/scenarios/sessions/streaming/typescript/src/index.ts b/test/scenarios/sessions/streaming/typescript/src/index.ts index 9cd530ebb..81d15041e 100644 --- a/test/scenarios/sessions/streaming/typescript/src/index.ts +++ b/test/scenarios/sessions/streaming/typescript/src/index.ts @@ -3,7 +3,6 @@ import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/custom-agents/csharp/Program.cs b/test/scenarios/tools/custom-agents/csharp/Program.cs index 6c5b980cc..9aa323c72 100644 --- a/test/scenarios/tools/custom-agents/csharp/Program.cs +++ b/test/scenarios/tools/custom-agents/csharp/Program.cs @@ -6,7 +6,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: cliPath), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/tools/custom-agents/go/main.go b/test/scenarios/tools/custom-agents/go/main.go index 1e6ada739..83f0908ee 100644 --- a/test/scenarios/tools/custom-agents/go/main.go +++ b/test/scenarios/tools/custom-agents/go/main.go @@ -4,15 +4,12 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -60,8 +57,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/tools/custom-agents/python/main.py b/test/scenarios/tools/custom-agents/python/main.py index aa5e254ae..1aaae199c 100644 --- a/test/scenarios/tools/custom-agents/python/main.py +++ b/test/scenarios/tools/custom-agents/python/main.py @@ -1,5 +1,4 @@ import asyncio -import os from copilot import CopilotClient from copilot.tools import Tool @@ -10,9 +9,7 @@ async def analyze_handler(args): async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/tools/custom-agents/rust/src/main.rs b/test/scenarios/tools/custom-agents/rust/src/main.rs index fe720b803..016ff47e6 100644 --- a/test/scenarios/tools/custom-agents/rust/src/main.rs +++ b/test/scenarios/tools/custom-agents/rust/src/main.rs @@ -19,9 +19,7 @@ struct AnalyzeParams { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let analyze_codebase = define_tool( "analyze-codebase", diff --git a/test/scenarios/tools/custom-agents/typescript/src/index.ts b/test/scenarios/tools/custom-agents/typescript/src/index.ts index db6dff214..86fe65804 100644 --- a/test/scenarios/tools/custom-agents/typescript/src/index.ts +++ b/test/scenarios/tools/custom-agents/typescript/src/index.ts @@ -12,7 +12,6 @@ const analyzeCodebase = defineTool("analyze-codebase", { async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/mcp-servers/csharp/Program.cs b/test/scenarios/tools/mcp-servers/csharp/Program.cs index ed667825f..de0c1b8c9 100644 --- a/test/scenarios/tools/mcp-servers/csharp/Program.cs +++ b/test/scenarios/tools/mcp-servers/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/tools/mcp-servers/go/main.go b/test/scenarios/tools/mcp-servers/go/main.go index b1a1225f1..57be90036 100644 --- a/test/scenarios/tools/mcp-servers/go/main.go +++ b/test/scenarios/tools/mcp-servers/go/main.go @@ -11,9 +11,7 @@ import ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -63,10 +61,10 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } if len(mcpServers) > 0 { keys := make([]string, 0, len(mcpServers)) diff --git a/test/scenarios/tools/mcp-servers/python/main.py b/test/scenarios/tools/mcp-servers/python/main.py index 80319e79a..706094ac9 100644 --- a/test/scenarios/tools/mcp-servers/python/main.py +++ b/test/scenarios/tools/mcp-servers/python/main.py @@ -5,9 +5,7 @@ async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: # MCP server config — demonstrates the configuration pattern. diff --git a/test/scenarios/tools/mcp-servers/rust/src/main.rs b/test/scenarios/tools/mcp-servers/rust/src/main.rs index 171d2bcd4..a1b043854 100644 --- a/test/scenarios/tools/mcp-servers/rust/src/main.rs +++ b/test/scenarios/tools/mcp-servers/rust/src/main.rs @@ -13,9 +13,7 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mcp_cmd = std::env::var("MCP_SERVER_CMD").ok(); let mcp_args_env = std::env::var("MCP_SERVER_ARGS").ok(); diff --git a/test/scenarios/tools/mcp-servers/typescript/src/index.ts b/test/scenarios/tools/mcp-servers/typescript/src/index.ts index 5117d3a64..cec67b45e 100644 --- a/test/scenarios/tools/mcp-servers/typescript/src/index.ts +++ b/test/scenarios/tools/mcp-servers/typescript/src/index.ts @@ -3,7 +3,6 @@ import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/no-tools/csharp/Program.cs b/test/scenarios/tools/no-tools/csharp/Program.cs index a0ea0eefe..ffaf28ebc 100644 --- a/test/scenarios/tools/no-tools/csharp/Program.cs +++ b/test/scenarios/tools/no-tools/csharp/Program.cs @@ -10,7 +10,6 @@ You can only respond with text based on your training data. using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/tools/no-tools/go/main.go b/test/scenarios/tools/no-tools/go/main.go index 5d1aa872f..352ac1c96 100644 --- a/test/scenarios/tools/no-tools/go/main.go +++ b/test/scenarios/tools/no-tools/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) @@ -15,9 +14,7 @@ You can only respond with text based on your training data. If asked about your capabilities or tools, clearly state that you have no tools available.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -46,8 +43,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/tools/no-tools/python/main.py b/test/scenarios/tools/no-tools/python/main.py index 61fa98ee1..35448e9a2 100644 --- a/test/scenarios/tools/no-tools/python/main.py +++ b/test/scenarios/tools/no-tools/python/main.py @@ -1,5 +1,4 @@ import asyncio -import os from copilot import CopilotClient @@ -10,9 +9,7 @@ async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/tools/no-tools/rust/src/main.rs b/test/scenarios/tools/no-tools/rust/src/main.rs index 64190c78b..c2e13339f 100644 --- a/test/scenarios/tools/no-tools/rust/src/main.rs +++ b/test/scenarios/tools/no-tools/rust/src/main.rs @@ -14,9 +14,7 @@ If asked about your capabilities or tools, clearly state that you have no tools #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); diff --git a/test/scenarios/tools/no-tools/typescript/src/index.ts b/test/scenarios/tools/no-tools/typescript/src/index.ts index 743aafe54..4f3ed0cd0 100644 --- a/test/scenarios/tools/no-tools/typescript/src/index.ts +++ b/test/scenarios/tools/no-tools/typescript/src/index.ts @@ -8,7 +8,6 @@ If asked about your capabilities or tools, clearly state that you have no tools async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/skills/csharp/Program.cs b/test/scenarios/tools/skills/csharp/Program.cs index 81adf96a5..72956bb99 100644 --- a/test/scenarios/tools/skills/csharp/Program.cs +++ b/test/scenarios/tools/skills/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/tools/skills/go/main.go b/test/scenarios/tools/skills/go/main.go index 7b0ef8032..02ebd18b2 100644 --- a/test/scenarios/tools/skills/go/main.go +++ b/test/scenarios/tools/skills/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "path/filepath" "runtime" @@ -12,9 +11,7 @@ import ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/tools/skills/python/main.py b/test/scenarios/tools/skills/python/main.py index 30b82fc1f..6b066bbf4 100644 --- a/test/scenarios/tools/skills/python/main.py +++ b/test/scenarios/tools/skills/python/main.py @@ -1,5 +1,4 @@ import asyncio -import os from pathlib import Path from copilot import CopilotClient @@ -7,9 +6,7 @@ async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: skills_dir = str(Path(__file__).resolve().parent.parent / "sample-skills") diff --git a/test/scenarios/tools/skills/rust/src/main.rs b/test/scenarios/tools/skills/rust/src/main.rs index d2f1ad6f0..64cf689a4 100644 --- a/test/scenarios/tools/skills/rust/src/main.rs +++ b/test/scenarios/tools/skills/rust/src/main.rs @@ -27,9 +27,7 @@ impl SessionHooks for AllowAllHooks { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; // CARGO_MANIFEST_DIR resolves to .../tools/skills/rust at compile time. let skills_dir: PathBuf = [env!("CARGO_MANIFEST_DIR"), "..", "sample-skills"] diff --git a/test/scenarios/tools/skills/typescript/src/index.ts b/test/scenarios/tools/skills/typescript/src/index.ts index 740adc587..890928b5e 100644 --- a/test/scenarios/tools/skills/typescript/src/index.ts +++ b/test/scenarios/tools/skills/typescript/src/index.ts @@ -7,7 +7,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/tool-filtering/csharp/Program.cs b/test/scenarios/tools/tool-filtering/csharp/Program.cs index 72431c005..f3eae076d 100644 --- a/test/scenarios/tools/tool-filtering/csharp/Program.cs +++ b/test/scenarios/tools/tool-filtering/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/tools/tool-filtering/go/main.go b/test/scenarios/tools/tool-filtering/go/main.go index e4a958be2..abe3cc6e4 100644 --- a/test/scenarios/tools/tool-filtering/go/main.go +++ b/test/scenarios/tools/tool-filtering/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) @@ -12,9 +11,7 @@ import ( const systemPrompt = `You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -43,8 +40,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/tools/tool-filtering/python/main.py b/test/scenarios/tools/tool-filtering/python/main.py index 711e8301e..a38f73c78 100644 --- a/test/scenarios/tools/tool-filtering/python/main.py +++ b/test/scenarios/tools/tool-filtering/python/main.py @@ -1,5 +1,4 @@ import asyncio -import os from copilot import CopilotClient @@ -7,9 +6,7 @@ async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/tools/tool-filtering/rust/src/main.rs b/test/scenarios/tools/tool-filtering/rust/src/main.rs index d4cd5d3c2..bce9b3aba 100644 --- a/test/scenarios/tools/tool-filtering/rust/src/main.rs +++ b/test/scenarios/tools/tool-filtering/rust/src/main.rs @@ -12,9 +12,7 @@ of tools. When asked about your tools, list exactly which tools you have availab #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut sysmsg = SystemMessageConfig::default(); sysmsg.mode = Some("replace".to_string()); diff --git a/test/scenarios/tools/tool-filtering/typescript/src/index.ts b/test/scenarios/tools/tool-filtering/typescript/src/index.ts index 87a86062e..b87b52f1e 100644 --- a/test/scenarios/tools/tool-filtering/typescript/src/index.ts +++ b/test/scenarios/tools/tool-filtering/typescript/src/index.ts @@ -3,7 +3,6 @@ import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/tool-overrides/csharp/Program.cs b/test/scenarios/tools/tool-overrides/csharp/Program.cs index a8c7679de..a3b0d1318 100644 --- a/test/scenarios/tools/tool-overrides/csharp/Program.cs +++ b/test/scenarios/tools/tool-overrides/csharp/Program.cs @@ -5,7 +5,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/tools/tool-overrides/go/main.go b/test/scenarios/tools/tool-overrides/go/main.go index 8d5f6a756..9a412c9d9 100644 --- a/test/scenarios/tools/tool-overrides/go/main.go +++ b/test/scenarios/tools/tool-overrides/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) @@ -14,9 +13,7 @@ type GrepParams struct { } func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -48,8 +45,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/tools/tool-overrides/python/main.py b/test/scenarios/tools/tool-overrides/python/main.py index 9aaaa9022..aa31de170 100644 --- a/test/scenarios/tools/tool-overrides/python/main.py +++ b/test/scenarios/tools/tool-overrides/python/main.py @@ -1,5 +1,4 @@ import asyncio -import os from pydantic import BaseModel, Field @@ -21,9 +20,7 @@ def custom_grep(params: GrepParams) -> str: async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/tools/tool-overrides/rust/src/main.rs b/test/scenarios/tools/tool-overrides/rust/src/main.rs index 5d5108724..bfa46b1b3 100644 --- a/test/scenarios/tools/tool-overrides/rust/src/main.rs +++ b/test/scenarios/tools/tool-overrides/rust/src/main.rs @@ -19,9 +19,7 @@ struct GrepParams { #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut grep_tool = define_tool( "grep", diff --git a/test/scenarios/tools/tool-overrides/typescript/src/index.ts b/test/scenarios/tools/tool-overrides/typescript/src/index.ts index fe6ff874f..bc1a26220 100644 --- a/test/scenarios/tools/tool-overrides/typescript/src/index.ts +++ b/test/scenarios/tools/tool-overrides/typescript/src/index.ts @@ -4,7 +4,6 @@ import { z } from "zod"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/tools/virtual-filesystem/csharp/Program.cs b/test/scenarios/tools/virtual-filesystem/csharp/Program.cs index 93ad41f1e..d967cd3e3 100644 --- a/test/scenarios/tools/virtual-filesystem/csharp/Program.cs +++ b/test/scenarios/tools/virtual-filesystem/csharp/Program.cs @@ -8,7 +8,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/tools/virtual-filesystem/go/main.go b/test/scenarios/tools/virtual-filesystem/go/main.go index de4b50637..faa315f0e 100644 --- a/test/scenarios/tools/virtual-filesystem/go/main.go +++ b/test/scenarios/tools/virtual-filesystem/go/main.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "log" - "os" "strings" "sync" @@ -73,9 +72,7 @@ func main() { }, } - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/tools/virtual-filesystem/python/main.py b/test/scenarios/tools/virtual-filesystem/python/main.py index 048ba1fd1..eeafa22ce 100644 --- a/test/scenarios/tools/virtual-filesystem/python/main.py +++ b/test/scenarios/tools/virtual-filesystem/python/main.py @@ -1,5 +1,4 @@ import asyncio -import os from pydantic import BaseModel, Field @@ -49,9 +48,7 @@ async def auto_approve_tool(input_data, invocation): async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session( diff --git a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts index 3fa21db00..7d0e0d040 100644 --- a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts +++ b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts @@ -40,7 +40,6 @@ const listFiles = defineTool("list_files", { async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/transport/reconnect/go/main.go b/test/scenarios/transport/reconnect/go/main.go index fda142316..2efeaa2db 100644 --- a/test/scenarios/transport/reconnect/go/main.go +++ b/test/scenarios/transport/reconnect/go/main.go @@ -38,10 +38,10 @@ func main() { } if response1 != nil { -if d, ok := response1.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} else { + if d, ok := response1.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } else { log.Fatal("No response content received for session 1") } @@ -66,10 +66,10 @@ fmt.Println(d.Content) } if response2 != nil { -if d, ok := response2.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} else { + if d, ok := response2.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } else { log.Fatal("No response content received for session 2") } diff --git a/test/scenarios/transport/stdio/csharp/Program.cs b/test/scenarios/transport/stdio/csharp/Program.cs index e9dfbcccc..576ca5518 100644 --- a/test/scenarios/transport/stdio/csharp/Program.cs +++ b/test/scenarios/transport/stdio/csharp/Program.cs @@ -3,7 +3,6 @@ using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), }); await client.StartAsync(); diff --git a/test/scenarios/transport/stdio/go/main.go b/test/scenarios/transport/stdio/go/main.go index 8fab8510d..c7716df2c 100644 --- a/test/scenarios/transport/stdio/go/main.go +++ b/test/scenarios/transport/stdio/go/main.go @@ -4,16 +4,13 @@ import ( "context" "fmt" "log" - "os" copilot "github.com/github/copilot-sdk/go" ) func main() { // Go SDK auto-reads COPILOT_CLI_PATH from env - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(&copilot.ClientOptions{}) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -37,8 +34,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/transport/stdio/python/main.py b/test/scenarios/transport/stdio/python/main.py index 63c309995..6be1d4294 100644 --- a/test/scenarios/transport/stdio/python/main.py +++ b/test/scenarios/transport/stdio/python/main.py @@ -1,13 +1,10 @@ import asyncio -import os from copilot import CopilotClient async def main(): - client = CopilotClient( - github_token=os.environ.get("GITHUB_TOKEN"), - ) + client = CopilotClient() try: session = await client.create_session(model="claude-haiku-4.5") diff --git a/test/scenarios/transport/stdio/rust/src/main.rs b/test/scenarios/transport/stdio/rust/src/main.rs index 2795a14fd..b3f92eaf9 100644 --- a/test/scenarios/transport/stdio/rust/src/main.rs +++ b/test/scenarios/transport/stdio/rust/src/main.rs @@ -8,9 +8,7 @@ use github_copilot_sdk::{Client, ClientOptions}; #[tokio::main] async fn main() -> Result<(), github_copilot_sdk::Error> { - let mut opts = ClientOptions::default(); - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); - let client = Client::start(opts).await?; + let client = Client::start(ClientOptions::default()).await?; let mut config = SessionConfig::default(); config.model = Some("claude-haiku-4.5".to_string()); diff --git a/test/scenarios/transport/stdio/typescript/src/index.ts b/test/scenarios/transport/stdio/typescript/src/index.ts index c80c1b074..38f937d59 100644 --- a/test/scenarios/transport/stdio/typescript/src/index.ts +++ b/test/scenarios/transport/stdio/typescript/src/index.ts @@ -3,7 +3,6 @@ import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - gitHubToken: process.env.GITHUB_TOKEN, }); try { diff --git a/test/scenarios/transport/tcp/go/main.go b/test/scenarios/transport/tcp/go/main.go index acdbaab76..95da9cf68 100644 --- a/test/scenarios/transport/tcp/go/main.go +++ b/test/scenarios/transport/tcp/go/main.go @@ -41,8 +41,8 @@ func main() { } if response != nil { -if d, ok := response.Data.(*copilot.AssistantMessageData); ok { -fmt.Println(d.Content) -} -} + if d, ok := response.Data.(*copilot.AssistantMessageData); ok { + fmt.Println(d.Content) + } + } } diff --git a/test/scenarios/transport/tcp/rust/src/main.rs b/test/scenarios/transport/tcp/rust/src/main.rs index 6488f243b..f9ccfe5f3 100644 --- a/test/scenarios/transport/tcp/rust/src/main.rs +++ b/test/scenarios/transport/tcp/rust/src/main.rs @@ -22,7 +22,6 @@ async fn main() -> Result<(), github_copilot_sdk::Error> { port, connection_token: None, }; - opts.github_token = std::env::var("GITHUB_TOKEN").ok(); let client = Client::start(opts).await?; let mut config = SessionConfig::default(); From 2a77f1240a5e6b7b854629dc445203dabb512876 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 15:05:33 +0100 Subject: [PATCH 02/14] TypeScript: remove getState() and public ConnectionState MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .NET removed `CopilotClient.State` and `ConnectionState` in commit b00fd8c9 (Phase 4d/4e). Python matched this in PR #1376. TypeScript was the last SDK still exposing both — drop them for cross-SDK consistency. - `CopilotClient.getState(): ConnectionState` removed. - Public `ConnectionState` type alias removed from `types.ts` and the index re-exports. - The private `state` field on `CopilotClient` stays (still used by start/stop to gate behavior); its type is now an inline union declaration instead of the exported alias. - Unit test for unexpected child-process death now reads the private `state` field via `(client as any).state` since the behaviour being tested (state transition on async disconnect) is genuinely internal. - E2E `client.getState() === "..."` assertions are dropped — they were pure tautologies once `start()` returned without throwing. - `nodejs/README.md` no longer documents `getState()`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/README.md | 4 ---- nodejs/src/client.ts | 19 +------------------ nodejs/src/index.ts | 1 - nodejs/src/types.ts | 5 ----- nodejs/test/client.test.ts | 4 ++-- nodejs/test/e2e/client.e2e.test.ts | 6 ------ nodejs/test/e2e/client_options.e2e.test.ts | 3 --- 7 files changed, 3 insertions(+), 39 deletions(-) diff --git a/nodejs/README.md b/nodejs/README.md index 06d88c752..ce08a1afc 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -131,10 +131,6 @@ Resume an existing session. Returns the session with `workspacePath` populated i Ping the server to check connectivity. -##### `getState(): ConnectionState` - -Get current connection state. - ##### `listSessions(filter?: SessionListFilter): Promise` List all available sessions. Optionally filter by working directory context. diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 904fb0ee2..abfa13a22 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -38,7 +38,6 @@ import { getTraceContext } from "./telemetry.js"; import type { AutoModeSwitchRequest, AutoModeSwitchResponse, - ConnectionState, CopilotClientOptions, CustomAgentConfig, ExitPlanModeRequest, @@ -251,7 +250,7 @@ export class CopilotClient { private socket: Socket | null = null; private runtimePort: number | null = null; private actualHost: string = "localhost"; - private state: ConnectionState = "disconnected"; + private state: "disconnected" | "connecting" | "connected" | "error" = "disconnected"; private sessions: Map = new Map(); private stderrBuffer: string = ""; // Captures CLI stderr for error messages /** Resolved connection mode chosen in the constructor. */ @@ -1039,22 +1038,6 @@ export class CopilotClient { return session; } - /** - * Gets the current connection state of the client. - * - * @returns The current connection state: "disconnected", "connecting", "connected", or "error" - * - * @example - * ```typescript - * if (client.getState() === "connected") { - * const session = await client.createSession({ onPermissionRequest: approveAll }); - * } - * ``` - */ - getState(): ConnectionState { - return this.state; - } - /** * Sends a ping request to the server to verify connectivity. * diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index 6ada0f141..e53dea036 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -40,7 +40,6 @@ export type { AutoModeSwitchHandler, AutoModeSwitchRequest, AutoModeSwitchResponse, - ConnectionState, CopilotClientOptions, StdioRuntimeConnection, TcpRuntimeConnection, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index fae3418a0..08782f9ac 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1842,11 +1842,6 @@ export type TypedSessionEventHandler = ( */ export type SessionEventHandler = (event: SessionEvent) => void; -/** - * Connection state - */ -export type ConnectionState = "disconnected" | "connecting" | "connected" | "error"; - /** * Working directory context for a session */ diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 49a2331a0..5670e13bd 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -984,7 +984,7 @@ describe("CopilotClient", () => { await client.start(); onTestFinished(() => client.forceStop()); - expect(client.getState()).toBe("connected"); + expect((client as any).state).toBe("connected"); // Kill the child process to simulate unexpected termination const proc = (client as any).cliProcess as import("node:child_process").ChildProcess; @@ -992,7 +992,7 @@ describe("CopilotClient", () => { // Wait for the connection.onClose handler to fire await vi.waitFor(() => { - expect(client.getState()).toBe("disconnected"); + expect((client as any).state).toBe("disconnected"); }); }); }); diff --git a/nodejs/test/e2e/client.e2e.test.ts b/nodejs/test/e2e/client.e2e.test.ts index b2021152c..33b7a0636 100644 --- a/nodejs/test/e2e/client.e2e.test.ts +++ b/nodejs/test/e2e/client.e2e.test.ts @@ -56,14 +56,12 @@ describe("Client", () => { onTestFinishedForceStop(client); await client.start(); - expect(client.getState()).toBe("connected"); const pong = await client.ping("test message"); expect(pong.message).toBe("pong: test message"); expect(Date.parse(pong.timestamp)).not.toBeNaN(); expect(await client.stop()).toHaveLength(0); // No errors on stop - expect(client.getState()).toBe("disconnected"); }); it("should start and connect to server using tcp", async () => { @@ -71,14 +69,12 @@ describe("Client", () => { onTestFinishedForceStop(client); await client.start(); - expect(client.getState()).toBe("connected"); const pong = await client.ping("test message"); expect(pong.message).toBe("pong: test message"); expect(Date.parse(pong.timestamp)).not.toBeNaN(); expect(await client.stop()).toHaveLength(0); // No errors on stop - expect(client.getState()).toBe("disconnected"); }); it.skipIf(process.platform === "darwin")( @@ -101,7 +97,6 @@ describe("Client", () => { await new Promise((resolve) => setTimeout(resolve, 100)); const errors = await client.stop(); - expect(client.getState()).toBe("disconnected"); if (errors.length > 0) { expect(errors[0].message).toContain("Failed to disconnect session"); } @@ -117,7 +112,6 @@ describe("Client", () => { await client.createSession({ onPermissionRequest: approveAll }); await client.forceStop(); - expect(client.getState()).toBe("disconnected"); }); it("should get status with version and protocol info", async () => { diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index 5174c9246..8c23ab046 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -154,10 +154,8 @@ describe("Client options", async () => { } }); - expect(client.getState()).toBe("disconnected"); const session = await client.createSession({ onPermissionRequest: approveAll }); - expect(client.getState()).toBe("connected"); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); await session.disconnect(); @@ -183,7 +181,6 @@ describe("Client options", async () => { await client.start(); - expect(client.getState()).toBe("connected"); expect((client as unknown as { runtimePort: number }).runtimePort).toBe(port); const response = await client.ping("fixed-port"); From f6a669d92f0163c42c6eb286b8008513eea129ef Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 15:12:23 +0100 Subject: [PATCH 03/14] Drop redundant RuntimeConnection + COPILOT_CLI_PATH from scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Most scenarios explicitly set up a stdio connection from COPILOT_CLI_PATH: - .NET: Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")) - TS: connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }) This is redundant boilerplate: - The default `RuntimeConnection` is already stdio across every SDK. - When no `path` is given, the SDKs already fall back to the `COPILOT_CLI_PATH` env var (and then the bundled binary), so the explicit pass-through adds nothing. - No real consumer of the SDK would write this — the docs reasonably expect `new CopilotClient()` (or the language-equivalent) to Just Work. Drop the connection setup entirely in every scenario *except* those whose whole purpose is to demonstrate transport / bundling configuration: - `transport/stdio` — explicit stdio path is the point. - `transport/tcp` / `transport/reconnect` — TCP transport is the point. - `bundling/*` — bundled-binary lookup is the point. Also clean up the resulting cruft: - Empty `new CopilotClientOptions {}` / `new CopilotClient({})` literals collapsed to parameter-less constructors. - Unused `RuntimeConnection` imports stripped from TS scenarios. - Go scenarios that previously passed `&copilot.ClientOptions{}` now pass `nil` (idiomatic when no fields are set). - The `cliPath` local var in `tools/custom-agents` (.NET) deleted. Rust scenarios keep `ClientOptions::default()` because the API requires the argument — that's idiomatic Rust, not redundant boilerplate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../auth/byok-anthropic/csharp/Program.cs | 5 +-- test/scenarios/auth/byok-anthropic/go/main.go | 2 +- .../byok-anthropic/typescript/src/index.ts | 6 +-- .../auth/byok-azure/csharp/Program.cs | 5 +-- test/scenarios/auth/byok-azure/go/main.go | 2 +- .../auth/byok-azure/typescript/src/index.ts | 6 +-- .../auth/byok-ollama/csharp/Program.cs | 5 +-- test/scenarios/auth/byok-ollama/go/main.go | 2 +- .../auth/byok-ollama/typescript/src/index.ts | 9 ++--- .../auth/byok-openai/csharp/Program.cs | 5 +-- test/scenarios/auth/byok-openai/go/main.go | 2 +- .../auth/byok-openai/typescript/src/index.ts | 9 ++--- test/scenarios/auth/gh-app/csharp/Program.cs | 1 - .../auth/gh-app/typescript/src/index.ts | 38 ++++++++++++++----- .../typescript/src/index.ts | 3 +- .../bundling/fully-bundled/go/main.go | 2 +- .../fully-bundled/typescript/src/index.ts | 6 ++- .../callbacks/hooks/csharp/Program.cs | 5 +-- test/scenarios/callbacks/hooks/go/main.go | 2 +- .../callbacks/hooks/typescript/src/index.ts | 9 ++--- .../callbacks/permissions/csharp/Program.cs | 5 +-- .../callbacks/permissions/go/main.go | 2 +- .../permissions/typescript/src/index.ts | 6 +-- .../callbacks/user-input/csharp/Program.cs | 5 +-- .../scenarios/callbacks/user-input/go/main.go | 2 +- .../user-input/typescript/src/index.ts | 9 ++--- .../scenarios/modes/default/csharp/Program.cs | 5 +-- test/scenarios/modes/default/go/main.go | 2 +- .../modes/default/typescript/src/index.ts | 9 ++--- .../scenarios/modes/minimal/csharp/Program.cs | 5 +-- test/scenarios/modes/minimal/go/main.go | 2 +- .../modes/minimal/typescript/src/index.ts | 6 +-- .../prompts/attachments/csharp/Program.cs | 5 +-- test/scenarios/prompts/attachments/go/main.go | 2 +- .../attachments/typescript/src/index.ts | 9 ++--- .../reasoning-effort/csharp/Program.cs | 5 +-- .../prompts/reasoning-effort/go/main.go | 2 +- .../reasoning-effort/typescript/src/index.ts | 6 +-- .../prompts/system-message/csharp/Program.cs | 5 +-- .../prompts/system-message/go/main.go | 2 +- .../system-message/typescript/src/index.ts | 6 +-- .../concurrent-sessions/csharp/Program.cs | 5 +-- .../sessions/concurrent-sessions/go/main.go | 2 +- .../typescript/src/index.ts | 6 +-- .../infinite-sessions/csharp/Program.cs | 5 +-- .../sessions/infinite-sessions/go/main.go | 2 +- .../infinite-sessions/typescript/src/index.ts | 15 ++++---- .../typescript/src/index.ts | 4 +- .../typescript/src/index.ts | 4 +- .../sessions/session-resume/csharp/Program.cs | 5 +-- .../sessions/session-resume/go/main.go | 2 +- .../session-resume/typescript/src/index.ts | 6 +-- .../sessions/streaming/csharp/Program.cs | 12 +----- test/scenarios/sessions/streaming/go/main.go | 2 +- .../streaming/typescript/src/index.ts | 6 +-- .../tools/custom-agents/csharp/Program.cs | 7 +--- test/scenarios/tools/custom-agents/go/main.go | 2 +- .../custom-agents/typescript/src/index.ts | 26 +++++++------ .../tools/mcp-servers/csharp/Program.cs | 5 +-- test/scenarios/tools/mcp-servers/go/main.go | 2 +- .../tools/mcp-servers/typescript/src/index.ts | 18 +++++---- .../tools/no-tools/csharp/Program.cs | 5 +-- test/scenarios/tools/no-tools/go/main.go | 2 +- .../tools/no-tools/typescript/src/index.ts | 6 +-- test/scenarios/tools/skills/csharp/Program.cs | 5 +-- test/scenarios/tools/skills/go/main.go | 2 +- .../tools/skills/typescript/src/index.ts | 6 +-- .../tools/tool-filtering/csharp/Program.cs | 5 +-- .../scenarios/tools/tool-filtering/go/main.go | 2 +- .../tool-filtering/typescript/src/index.ts | 9 ++--- .../tools/tool-overrides/csharp/Program.cs | 5 +-- .../scenarios/tools/tool-overrides/go/main.go | 2 +- .../tool-overrides/typescript/src/index.ts | 9 ++--- .../virtual-filesystem/csharp/Program.cs | 5 +-- .../tools/virtual-filesystem/go/main.go | 2 +- .../typescript/src/index.ts | 9 ++--- .../reconnect/typescript/src/index.ts | 8 +++- test/scenarios/transport/stdio/go/main.go | 2 +- .../transport/stdio/typescript/src/index.ts | 6 ++- .../transport/tcp/typescript/src/index.ts | 4 +- 80 files changed, 192 insertions(+), 262 deletions(-) diff --git a/test/scenarios/auth/byok-anthropic/csharp/Program.cs b/test/scenarios/auth/byok-anthropic/csharp/Program.cs index f29cfd4d8..3aac89577 100644 --- a/test/scenarios/auth/byok-anthropic/csharp/Program.cs +++ b/test/scenarios/auth/byok-anthropic/csharp/Program.cs @@ -10,10 +10,7 @@ return 1; } -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/auth/byok-anthropic/go/main.go b/test/scenarios/auth/byok-anthropic/go/main.go index 98a67aa76..efe7d5b4d 100644 --- a/test/scenarios/auth/byok-anthropic/go/main.go +++ b/test/scenarios/auth/byok-anthropic/go/main.go @@ -25,7 +25,7 @@ func main() { model = "claude-sonnet-4-20250514" } - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/auth/byok-anthropic/typescript/src/index.ts b/test/scenarios/auth/byok-anthropic/typescript/src/index.ts index bb60158c2..67eb27dd8 100644 --- a/test/scenarios/auth/byok-anthropic/typescript/src/index.ts +++ b/test/scenarios/auth/byok-anthropic/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { const apiKey = process.env.ANTHROPIC_API_KEY; @@ -9,9 +9,7 @@ async function main() { process.exit(1); } - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/auth/byok-azure/csharp/Program.cs b/test/scenarios/auth/byok-azure/csharp/Program.cs index 64132bbff..635404843 100644 --- a/test/scenarios/auth/byok-azure/csharp/Program.cs +++ b/test/scenarios/auth/byok-azure/csharp/Program.cs @@ -11,10 +11,7 @@ return 1; } -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/auth/byok-azure/go/main.go b/test/scenarios/auth/byok-azure/go/main.go index 6050929ac..eea3fb8d6 100644 --- a/test/scenarios/auth/byok-azure/go/main.go +++ b/test/scenarios/auth/byok-azure/go/main.go @@ -26,7 +26,7 @@ func main() { apiVersion = "2024-10-21" } - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/auth/byok-azure/typescript/src/index.ts b/test/scenarios/auth/byok-azure/typescript/src/index.ts index 14d4e5ced..8df0e4de3 100644 --- a/test/scenarios/auth/byok-azure/typescript/src/index.ts +++ b/test/scenarios/auth/byok-azure/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { const endpoint = process.env.AZURE_OPENAI_ENDPOINT; @@ -10,9 +10,7 @@ async function main() { process.exit(1); } - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/auth/byok-ollama/csharp/Program.cs b/test/scenarios/auth/byok-ollama/csharp/Program.cs index 69578a378..62b000af1 100644 --- a/test/scenarios/auth/byok-ollama/csharp/Program.cs +++ b/test/scenarios/auth/byok-ollama/csharp/Program.cs @@ -6,10 +6,7 @@ var compactSystemPrompt = "You are a compact local assistant. Keep answers short, concrete, and under 80 words."; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/auth/byok-ollama/go/main.go b/test/scenarios/auth/byok-ollama/go/main.go index a507ba558..c776da27b 100644 --- a/test/scenarios/auth/byok-ollama/go/main.go +++ b/test/scenarios/auth/byok-ollama/go/main.go @@ -22,7 +22,7 @@ func main() { model = "llama3.2:3b" } - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/auth/byok-ollama/typescript/src/index.ts b/test/scenarios/auth/byok-ollama/typescript/src/index.ts index 7db9dd81c..af2d71a44 100644 --- a/test/scenarios/auth/byok-ollama/typescript/src/index.ts +++ b/test/scenarios/auth/byok-ollama/typescript/src/index.ts @@ -1,15 +1,14 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; -const OLLAMA_BASE_URL = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434/v1"; +const OLLAMA_BASE_URL = + process.env.OLLAMA_BASE_URL ?? "http://localhost:11434/v1"; const OLLAMA_MODEL = process.env.OLLAMA_MODEL ?? "llama3.2:3b"; const COMPACT_SYSTEM_PROMPT = "You are a compact local assistant. Keep answers short, concrete, and under 80 words."; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/auth/byok-openai/csharp/Program.cs b/test/scenarios/auth/byok-openai/csharp/Program.cs index d98cffbc3..826e35443 100644 --- a/test/scenarios/auth/byok-openai/csharp/Program.cs +++ b/test/scenarios/auth/byok-openai/csharp/Program.cs @@ -10,10 +10,7 @@ return 1; } -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/auth/byok-openai/go/main.go b/test/scenarios/auth/byok-openai/go/main.go index d1149a067..d3221523d 100644 --- a/test/scenarios/auth/byok-openai/go/main.go +++ b/test/scenarios/auth/byok-openai/go/main.go @@ -25,7 +25,7 @@ func main() { model = "claude-haiku-4.5" } - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/auth/byok-openai/typescript/src/index.ts b/test/scenarios/auth/byok-openai/typescript/src/index.ts index 1b69fc665..268f1d201 100644 --- a/test/scenarios/auth/byok-openai/typescript/src/index.ts +++ b/test/scenarios/auth/byok-openai/typescript/src/index.ts @@ -1,6 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; -const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"; +const OPENAI_BASE_URL = + process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"; const OPENAI_MODEL = process.env.OPENAI_MODEL ?? "claude-haiku-4.5"; const OPENAI_API_KEY = process.env.OPENAI_API_KEY; @@ -10,9 +11,7 @@ if (!OPENAI_API_KEY) { } async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/auth/gh-app/csharp/Program.cs b/test/scenarios/auth/gh-app/csharp/Program.cs index 5933ec087..f16c8236e 100644 --- a/test/scenarios/auth/gh-app/csharp/Program.cs +++ b/test/scenarios/auth/gh-app/csharp/Program.cs @@ -60,7 +60,6 @@ // Step 4: Use the token with Copilot using var client = new CopilotClient(new CopilotClientOptions { - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), GitHubToken = accessToken, }); diff --git a/test/scenarios/auth/gh-app/typescript/src/index.ts b/test/scenarios/auth/gh-app/typescript/src/index.ts index bfd53898c..b76fdc0a2 100644 --- a/test/scenarios/auth/gh-app/typescript/src/index.ts +++ b/test/scenarios/auth/gh-app/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; import readline from "node:readline/promises"; import { stdin as input, stdout as output } from "node:process"; @@ -33,7 +33,10 @@ if (!CLIENT_ID) { process.exit(1); } -async function postJson(url: string, body: Record): Promise { +async function postJson( + url: string, + body: Record, +): Promise { const response = await fetch(url, { method: "POST", headers: { @@ -44,7 +47,9 @@ async function postJson(url: string, body: Record): Promise< }); if (!response.ok) { - throw new Error(`Request failed: ${response.status} ${response.statusText}`); + throw new Error( + `Request failed: ${response.status} ${response.statusText}`, + ); } return (await response.json()) as T; @@ -60,7 +65,9 @@ async function getJson(url: string, token: string): Promise { }); if (!response.ok) { - throw new Error(`GitHub API failed: ${response.status} ${response.statusText}`); + throw new Error( + `GitHub API failed: ${response.status} ${response.statusText}`, + ); } return (await response.json()) as T; @@ -73,7 +80,10 @@ async function startDeviceFlow(): Promise { }); } -async function pollForAccessToken(deviceCode: string, intervalSeconds: number): Promise { +async function pollForAccessToken( + deviceCode: string, + intervalSeconds: number, +): Promise { let interval = intervalSeconds; while (true) { @@ -92,7 +102,9 @@ async function pollForAccessToken(deviceCode: string, intervalSeconds: number): continue; } - throw new Error(data.error_description ?? data.error ?? "OAuth token polling failed"); + throw new Error( + data.error_description ?? data.error ?? "OAuth token polling failed", + ); } } @@ -100,17 +112,23 @@ async function main() { console.log("Starting GitHub OAuth device flow..."); const device = await startDeviceFlow(); - console.log(`Open ${device.verification_uri} and enter code: ${device.user_code}`); + console.log( + `Open ${device.verification_uri} and enter code: ${device.user_code}`, + ); const rl = readline.createInterface({ input, output }); await rl.question("Press Enter after you authorize this app..."); rl.close(); - const accessToken = await pollForAccessToken(device.device_code, device.interval); + const accessToken = await pollForAccessToken( + device.device_code, + device.interval, + ); const user = await getJson(USER_URL, accessToken); - console.log(`Authenticated as: ${user.login}${user.name ? ` (${user.name})` : ""}`); + console.log( + `Authenticated as: ${user.login}${user.name ? ` (${user.name})` : ""}`, + ); const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), gitHubToken: accessToken, }); diff --git a/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts b/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts index 7ab734d1a..c169e7f89 100644 --- a/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts +++ b/test/scenarios/bundling/app-backend-to-server/typescript/src/index.ts @@ -2,7 +2,8 @@ import express from "express"; import { CopilotClient } from "@github/copilot-sdk"; const PORT = parseInt(process.env.PORT || "8080", 10); -const CLI_URL = process.env.CLI_URL || process.env.COPILOT_CLI_URL || "localhost:3000"; +const CLI_URL = + process.env.CLI_URL || process.env.COPILOT_CLI_URL || "localhost:3000"; const app = express(); app.use(express.json()); diff --git a/test/scenarios/bundling/fully-bundled/go/main.go b/test/scenarios/bundling/fully-bundled/go/main.go index c7716df2c..51b592431 100644 --- a/test/scenarios/bundling/fully-bundled/go/main.go +++ b/test/scenarios/bundling/fully-bundled/go/main.go @@ -10,7 +10,7 @@ import ( func main() { // Go SDK auto-reads COPILOT_CLI_PATH from env - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts index 38f937d59..7df9cd888 100644 --- a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts +++ b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts @@ -1,8 +1,10 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + connection: RuntimeConnection.forStdio({ + path: process.env.COPILOT_CLI_PATH, + }), }); try { diff --git a/test/scenarios/callbacks/hooks/csharp/Program.cs b/test/scenarios/callbacks/hooks/csharp/Program.cs index 3927e4c95..1325565e8 100644 --- a/test/scenarios/callbacks/hooks/csharp/Program.cs +++ b/test/scenarios/callbacks/hooks/csharp/Program.cs @@ -2,10 +2,7 @@ var hookLog = new List(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/callbacks/hooks/go/main.go b/test/scenarios/callbacks/hooks/go/main.go index a7f9650b3..01411101e 100644 --- a/test/scenarios/callbacks/hooks/go/main.go +++ b/test/scenarios/callbacks/hooks/go/main.go @@ -21,7 +21,7 @@ func main() { hookLogMu.Unlock() } - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/callbacks/hooks/typescript/src/index.ts b/test/scenarios/callbacks/hooks/typescript/src/index.ts index e0fdad1d6..c712994a9 100644 --- a/test/scenarios/callbacks/hooks/typescript/src/index.ts +++ b/test/scenarios/callbacks/hooks/typescript/src/index.ts @@ -1,11 +1,9 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { const hookLog: string[] = []; - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -36,7 +34,8 @@ async function main() { }); const response = await session.sendAndWait({ - prompt: "List the files in the current directory using the glob tool with pattern '*.md'.", + prompt: + "List the files in the current directory using the glob tool with pattern '*.md'.", }); if (response) { diff --git a/test/scenarios/callbacks/permissions/csharp/Program.cs b/test/scenarios/callbacks/permissions/csharp/Program.cs index 683cfcda4..1e309e71a 100644 --- a/test/scenarios/callbacks/permissions/csharp/Program.cs +++ b/test/scenarios/callbacks/permissions/csharp/Program.cs @@ -2,10 +2,7 @@ var permissionLog = new List(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/callbacks/permissions/go/main.go b/test/scenarios/callbacks/permissions/go/main.go index 1397d9ece..489db10e1 100644 --- a/test/scenarios/callbacks/permissions/go/main.go +++ b/test/scenarios/callbacks/permissions/go/main.go @@ -15,7 +15,7 @@ func main() { permissionLogMu sync.Mutex ) - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/callbacks/permissions/typescript/src/index.ts b/test/scenarios/callbacks/permissions/typescript/src/index.ts index 04cce2713..861f5a654 100644 --- a/test/scenarios/callbacks/permissions/typescript/src/index.ts +++ b/test/scenarios/callbacks/permissions/typescript/src/index.ts @@ -1,11 +1,9 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { const permissionLog: string[] = []; - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/callbacks/user-input/csharp/Program.cs b/test/scenarios/callbacks/user-input/csharp/Program.cs index 4455622a7..a89d2cd98 100644 --- a/test/scenarios/callbacks/user-input/csharp/Program.cs +++ b/test/scenarios/callbacks/user-input/csharp/Program.cs @@ -2,10 +2,7 @@ var inputLog = new List(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/callbacks/user-input/go/main.go b/test/scenarios/callbacks/user-input/go/main.go index a4fcdfedf..f3c58e253 100644 --- a/test/scenarios/callbacks/user-input/go/main.go +++ b/test/scenarios/callbacks/user-input/go/main.go @@ -15,7 +15,7 @@ var ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/callbacks/user-input/typescript/src/index.ts b/test/scenarios/callbacks/user-input/typescript/src/index.ts index c41f9cc3a..e7e3d0bca 100644 --- a/test/scenarios/callbacks/user-input/typescript/src/index.ts +++ b/test/scenarios/callbacks/user-input/typescript/src/index.ts @@ -1,11 +1,9 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { const inputLog: string[] = []; - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -21,7 +19,8 @@ async function main() { }); const response = await session.sendAndWait({ - prompt: "I want to learn about a city. Use the ask_user tool to ask me which city I'm interested in. Then tell me about that city.", + prompt: + "I want to learn about a city. Use the ask_user tool to ask me which city I'm interested in. Then tell me about that city.", }); if (response) { diff --git a/test/scenarios/modes/default/csharp/Program.cs b/test/scenarios/modes/default/csharp/Program.cs index 41a33a379..71ea173aa 100644 --- a/test/scenarios/modes/default/csharp/Program.cs +++ b/test/scenarios/modes/default/csharp/Program.cs @@ -1,9 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/modes/default/go/main.go b/test/scenarios/modes/default/go/main.go index e2b5f3634..7cefdcf26 100644 --- a/test/scenarios/modes/default/go/main.go +++ b/test/scenarios/modes/default/go/main.go @@ -9,7 +9,7 @@ import ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/modes/default/typescript/src/index.ts b/test/scenarios/modes/default/typescript/src/index.ts index 937970e2c..c6db7562d 100644 --- a/test/scenarios/modes/default/typescript/src/index.ts +++ b/test/scenarios/modes/default/typescript/src/index.ts @@ -1,9 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -11,7 +9,8 @@ async function main() { }); const response = await session.sendAndWait({ - prompt: "Use the grep tool to search for the word 'SDK' in README.md and show the matching lines.", + prompt: + "Use the grep tool to search for the word 'SDK' in README.md and show the matching lines.", }); if (response) { diff --git a/test/scenarios/modes/minimal/csharp/Program.cs b/test/scenarios/modes/minimal/csharp/Program.cs index 44a0d31e2..166048d34 100644 --- a/test/scenarios/modes/minimal/csharp/Program.cs +++ b/test/scenarios/modes/minimal/csharp/Program.cs @@ -1,9 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/modes/minimal/go/main.go b/test/scenarios/modes/minimal/go/main.go index 909938139..4cb891885 100644 --- a/test/scenarios/modes/minimal/go/main.go +++ b/test/scenarios/modes/minimal/go/main.go @@ -9,7 +9,7 @@ import ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/modes/minimal/typescript/src/index.ts b/test/scenarios/modes/minimal/typescript/src/index.ts index 54e9a226f..68fa73752 100644 --- a/test/scenarios/modes/minimal/typescript/src/index.ts +++ b/test/scenarios/modes/minimal/typescript/src/index.ts @@ -1,9 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/prompts/attachments/csharp/Program.cs b/test/scenarios/prompts/attachments/csharp/Program.cs index c98bf1f65..8983af2d9 100644 --- a/test/scenarios/prompts/attachments/csharp/Program.cs +++ b/test/scenarios/prompts/attachments/csharp/Program.cs @@ -1,9 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/prompts/attachments/go/main.go b/test/scenarios/prompts/attachments/go/main.go index c29294e56..4acfee001 100644 --- a/test/scenarios/prompts/attachments/go/main.go +++ b/test/scenarios/prompts/attachments/go/main.go @@ -13,7 +13,7 @@ import ( const systemPrompt = `You are a helpful assistant. Answer questions about attached files concisely.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/prompts/attachments/typescript/src/index.ts b/test/scenarios/prompts/attachments/typescript/src/index.ts index 65f388205..3de4b757a 100644 --- a/test/scenarios/prompts/attachments/typescript/src/index.ts +++ b/test/scenarios/prompts/attachments/typescript/src/index.ts @@ -1,13 +1,11 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -15,7 +13,8 @@ async function main() { availableTools: [], systemMessage: { mode: "replace", - content: "You are a helpful assistant. Answer questions about attached files concisely.", + content: + "You are a helpful assistant. Answer questions about attached files concisely.", }, }); diff --git a/test/scenarios/prompts/reasoning-effort/csharp/Program.cs b/test/scenarios/prompts/reasoning-effort/csharp/Program.cs index bef7fa6f5..36dc52e44 100644 --- a/test/scenarios/prompts/reasoning-effort/csharp/Program.cs +++ b/test/scenarios/prompts/reasoning-effort/csharp/Program.cs @@ -1,9 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/prompts/reasoning-effort/go/main.go b/test/scenarios/prompts/reasoning-effort/go/main.go index 5ce6a4a11..ca3af77d3 100644 --- a/test/scenarios/prompts/reasoning-effort/go/main.go +++ b/test/scenarios/prompts/reasoning-effort/go/main.go @@ -9,7 +9,7 @@ import ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts index 300051f5e..da937738d 100644 --- a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts +++ b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts @@ -1,9 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { // Test with "low" reasoning effort diff --git a/test/scenarios/prompts/system-message/csharp/Program.cs b/test/scenarios/prompts/system-message/csharp/Program.cs index 03d0ad185..cebd2417c 100644 --- a/test/scenarios/prompts/system-message/csharp/Program.cs +++ b/test/scenarios/prompts/system-message/csharp/Program.cs @@ -2,10 +2,7 @@ var piratePrompt = "You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout."; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/prompts/system-message/go/main.go b/test/scenarios/prompts/system-message/go/main.go index 138a14e92..d0b1aee5e 100644 --- a/test/scenarios/prompts/system-message/go/main.go +++ b/test/scenarios/prompts/system-message/go/main.go @@ -11,7 +11,7 @@ import ( const piratePrompt = `You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/prompts/system-message/typescript/src/index.ts b/test/scenarios/prompts/system-message/typescript/src/index.ts index 892c0a517..27525a28f 100644 --- a/test/scenarios/prompts/system-message/typescript/src/index.ts +++ b/test/scenarios/prompts/system-message/typescript/src/index.ts @@ -1,11 +1,9 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; const PIRATE_PROMPT = `You are a pirate. Always respond in pirate speak. Say 'Arrr!' in every response. Use nautical terms and pirate slang throughout.`; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs b/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs index 419804bd8..d53dc41f1 100644 --- a/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs +++ b/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs @@ -3,10 +3,7 @@ const string PiratePrompt = "You are a pirate. Always say Arrr!"; const string RobotPrompt = "You are a robot. Always say BEEP BOOP!"; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/sessions/concurrent-sessions/go/main.go b/test/scenarios/sessions/concurrent-sessions/go/main.go index ffcc5eb6d..342d0f6cd 100644 --- a/test/scenarios/sessions/concurrent-sessions/go/main.go +++ b/test/scenarios/sessions/concurrent-sessions/go/main.go @@ -13,7 +13,7 @@ const piratePrompt = `You are a pirate. Always say Arrr!` const robotPrompt = `You are a robot. Always say BEEP BOOP!` func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts index 39892b0ca..196249e82 100644 --- a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts @@ -1,12 +1,10 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; const PIRATE_PROMPT = `You are a pirate. Always say Arrr!`; const ROBOT_PROMPT = `You are a robot. Always say BEEP BOOP!`; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const [session1, session2] = await Promise.all([ diff --git a/test/scenarios/sessions/infinite-sessions/csharp/Program.cs b/test/scenarios/sessions/infinite-sessions/csharp/Program.cs index 4b81a99e2..f5a2ff183 100644 --- a/test/scenarios/sessions/infinite-sessions/csharp/Program.cs +++ b/test/scenarios/sessions/infinite-sessions/csharp/Program.cs @@ -1,9 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/sessions/infinite-sessions/go/main.go b/test/scenarios/sessions/infinite-sessions/go/main.go index 134239b0a..62d640850 100644 --- a/test/scenarios/sessions/infinite-sessions/go/main.go +++ b/test/scenarios/sessions/infinite-sessions/go/main.go @@ -12,7 +12,7 @@ func boolPtr(b bool) *bool { return &b } func float64Ptr(f float64) *float64 { return &f } func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts index da573b561..f10543ab0 100644 --- a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts @@ -1,9 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -11,11 +9,12 @@ async function main() { availableTools: [], systemMessage: { mode: "replace", - content: "You are a helpful assistant. Answer concisely in one sentence.", + content: + "You are a helpful assistant. Answer concisely in one sentence.", }, infiniteSessions: { enabled: true, - backgroundCompactionThreshold: 0.80, + backgroundCompactionThreshold: 0.8, bufferExhaustionThreshold: 0.95, }, }); @@ -34,7 +33,9 @@ async function main() { } } - console.log("Infinite sessions test complete — all messages processed successfully"); + console.log( + "Infinite sessions test complete — all messages processed successfully", + ); await session.disconnect(); } finally { diff --git a/test/scenarios/sessions/multi-user-long-lived/typescript/src/index.ts b/test/scenarios/sessions/multi-user-long-lived/typescript/src/index.ts index 2071da484..5ab3ca45c 100644 --- a/test/scenarios/sessions/multi-user-long-lived/typescript/src/index.ts +++ b/test/scenarios/sessions/multi-user-long-lived/typescript/src/index.ts @@ -1,2 +1,4 @@ -console.log("SKIP: multi-user-long-lived requires memory FS and preset features which is not supported by the old SDK"); +console.log( + "SKIP: multi-user-long-lived requires memory FS and preset features which is not supported by the old SDK", +); process.exit(0); diff --git a/test/scenarios/sessions/multi-user-short-lived/typescript/src/index.ts b/test/scenarios/sessions/multi-user-short-lived/typescript/src/index.ts index eeaceb458..4cfbd5e72 100644 --- a/test/scenarios/sessions/multi-user-short-lived/typescript/src/index.ts +++ b/test/scenarios/sessions/multi-user-short-lived/typescript/src/index.ts @@ -1,2 +1,4 @@ -console.log("SKIP: multi-user-short-lived requires memory FS and preset features which is not supported by the old SDK"); +console.log( + "SKIP: multi-user-short-lived requires memory FS and preset features which is not supported by the old SDK", +); process.exit(0); diff --git a/test/scenarios/sessions/session-resume/csharp/Program.cs b/test/scenarios/sessions/session-resume/csharp/Program.cs index 327628755..4f8bbdb5a 100644 --- a/test/scenarios/sessions/session-resume/csharp/Program.cs +++ b/test/scenarios/sessions/session-resume/csharp/Program.cs @@ -1,9 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/sessions/session-resume/go/main.go b/test/scenarios/sessions/session-resume/go/main.go index aca5cb16d..365c58ec6 100644 --- a/test/scenarios/sessions/session-resume/go/main.go +++ b/test/scenarios/sessions/session-resume/go/main.go @@ -9,7 +9,7 @@ import ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/sessions/session-resume/typescript/src/index.ts b/test/scenarios/sessions/session-resume/typescript/src/index.ts index 25b87c628..125b89341 100644 --- a/test/scenarios/sessions/session-resume/typescript/src/index.ts +++ b/test/scenarios/sessions/session-resume/typescript/src/index.ts @@ -1,9 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { // 1. Create a session diff --git a/test/scenarios/sessions/streaming/csharp/Program.cs b/test/scenarios/sessions/streaming/csharp/Program.cs index 8a0300840..706960901 100644 --- a/test/scenarios/sessions/streaming/csharp/Program.cs +++ b/test/scenarios/sessions/streaming/csharp/Program.cs @@ -1,16 +1,6 @@ using GitHub.Copilot; -var options = new CopilotClientOptions -{ -}; - -var cliPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); -if (!string.IsNullOrEmpty(cliPath)) -{ - options.Connection = RuntimeConnection.ForStdio(path: cliPath); -} - -using var client = new CopilotClient(options); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/sessions/streaming/go/main.go b/test/scenarios/sessions/streaming/go/main.go index edf70c801..e2a11029a 100644 --- a/test/scenarios/sessions/streaming/go/main.go +++ b/test/scenarios/sessions/streaming/go/main.go @@ -9,7 +9,7 @@ import ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/sessions/streaming/typescript/src/index.ts b/test/scenarios/sessions/streaming/typescript/src/index.ts index 81d15041e..25df8cb4b 100644 --- a/test/scenarios/sessions/streaming/typescript/src/index.ts +++ b/test/scenarios/sessions/streaming/typescript/src/index.ts @@ -1,9 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/tools/custom-agents/csharp/Program.cs b/test/scenarios/tools/custom-agents/csharp/Program.cs index 9aa323c72..f2db5fe5b 100644 --- a/test/scenarios/tools/custom-agents/csharp/Program.cs +++ b/test/scenarios/tools/custom-agents/csharp/Program.cs @@ -1,12 +1,7 @@ using GitHub.Copilot; using Microsoft.Extensions.AI; -var cliPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); - -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: cliPath), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/custom-agents/go/main.go b/test/scenarios/tools/custom-agents/go/main.go index 83f0908ee..e52404a6a 100644 --- a/test/scenarios/tools/custom-agents/go/main.go +++ b/test/scenarios/tools/custom-agents/go/main.go @@ -9,7 +9,7 @@ import ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/tools/custom-agents/typescript/src/index.ts b/test/scenarios/tools/custom-agents/typescript/src/index.ts index 86fe65804..4a48902f8 100644 --- a/test/scenarios/tools/custom-agents/typescript/src/index.ts +++ b/test/scenarios/tools/custom-agents/typescript/src/index.ts @@ -1,18 +1,17 @@ -import { CopilotClient, defineTool , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient, defineTool } from "@github/copilot-sdk"; import { z } from "zod"; const analyzeCodebase = defineTool("analyze-codebase", { - description: "Performs deep analysis of the codebase, generating extensive context", - parameters: z.object({ query: z.string().describe("The analysis query") }), - handler: async ({ query }) => { - return `Analysis result for: ${query}`; - }, + description: + "Performs deep analysis of the codebase, generating extensive context", + parameters: z.object({ query: z.string().describe("The analysis query") }), + handler: async ({ query }) => { + return `Analysis result for: ${query}`; + }, }); async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -25,15 +24,18 @@ async function main() { { name: "researcher", displayName: "Research Agent", - description: "A research agent that can only read and search files, not modify them", + description: + "A research agent that can only read and search files, not modify them", tools: ["grep", "glob", "view", "analyze-codebase"], - prompt: "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.", + prompt: + "You are a research assistant. You can search and read files but cannot modify anything. When asked about your capabilities, list the tools you have access to.", }, ], }); const response = await session.sendAndWait({ - prompt: "What custom agents are available? Describe the researcher agent and its capabilities.", + prompt: + "What custom agents are available? Describe the researcher agent and its capabilities.", }); if (response) { diff --git a/test/scenarios/tools/mcp-servers/csharp/Program.cs b/test/scenarios/tools/mcp-servers/csharp/Program.cs index de0c1b8c9..7d7fe4738 100644 --- a/test/scenarios/tools/mcp-servers/csharp/Program.cs +++ b/test/scenarios/tools/mcp-servers/csharp/Program.cs @@ -1,9 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/mcp-servers/go/main.go b/test/scenarios/tools/mcp-servers/go/main.go index 57be90036..dd6ec5219 100644 --- a/test/scenarios/tools/mcp-servers/go/main.go +++ b/test/scenarios/tools/mcp-servers/go/main.go @@ -11,7 +11,7 @@ import ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/tools/mcp-servers/typescript/src/index.ts b/test/scenarios/tools/mcp-servers/typescript/src/index.ts index cec67b45e..838094c8d 100644 --- a/test/scenarios/tools/mcp-servers/typescript/src/index.ts +++ b/test/scenarios/tools/mcp-servers/typescript/src/index.ts @@ -1,9 +1,7 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { // MCP server config — demonstrates the configuration pattern. @@ -14,7 +12,9 @@ async function main() { mcpServers["example"] = { type: "stdio", command: process.env.MCP_SERVER_CMD, - args: process.env.MCP_SERVER_ARGS ? process.env.MCP_SERVER_ARGS.split(" ") : [], + args: process.env.MCP_SERVER_ARGS + ? process.env.MCP_SERVER_ARGS.split(" ") + : [], }; } @@ -37,9 +37,13 @@ async function main() { } if (Object.keys(mcpServers).length > 0) { - console.log("\nMCP servers configured: " + Object.keys(mcpServers).join(", ")); + console.log( + "\nMCP servers configured: " + Object.keys(mcpServers).join(", "), + ); } else { - console.log("\nNo MCP servers configured (set MCP_SERVER_CMD to test with a real server)"); + console.log( + "\nNo MCP servers configured (set MCP_SERVER_CMD to test with a real server)", + ); } await session.disconnect(); diff --git a/test/scenarios/tools/no-tools/csharp/Program.cs b/test/scenarios/tools/no-tools/csharp/Program.cs index ffaf28ebc..a9a2e0308 100644 --- a/test/scenarios/tools/no-tools/csharp/Program.cs +++ b/test/scenarios/tools/no-tools/csharp/Program.cs @@ -7,10 +7,7 @@ You can only respond with text based on your training data. If asked about your capabilities or tools, clearly state that you have no tools available. """; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/no-tools/go/main.go b/test/scenarios/tools/no-tools/go/main.go index 352ac1c96..9698ebded 100644 --- a/test/scenarios/tools/no-tools/go/main.go +++ b/test/scenarios/tools/no-tools/go/main.go @@ -14,7 +14,7 @@ You can only respond with text based on your training data. If asked about your capabilities or tools, clearly state that you have no tools available.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/tools/no-tools/typescript/src/index.ts b/test/scenarios/tools/no-tools/typescript/src/index.ts index 4f3ed0cd0..5756bb350 100644 --- a/test/scenarios/tools/no-tools/typescript/src/index.ts +++ b/test/scenarios/tools/no-tools/typescript/src/index.ts @@ -1,4 +1,4 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; const SYSTEM_PROMPT = `You are a minimal assistant with no tools available. You cannot execute code, read files, edit files, search, or perform any actions. @@ -6,9 +6,7 @@ You can only respond with text based on your training data. If asked about your capabilities or tools, clearly state that you have no tools available.`; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/tools/skills/csharp/Program.cs b/test/scenarios/tools/skills/csharp/Program.cs index 72956bb99..5436619aa 100644 --- a/test/scenarios/tools/skills/csharp/Program.cs +++ b/test/scenarios/tools/skills/csharp/Program.cs @@ -1,9 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/skills/go/main.go b/test/scenarios/tools/skills/go/main.go index 02ebd18b2..57bb9ce14 100644 --- a/test/scenarios/tools/skills/go/main.go +++ b/test/scenarios/tools/skills/go/main.go @@ -11,7 +11,7 @@ import ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/tools/skills/typescript/src/index.ts b/test/scenarios/tools/skills/typescript/src/index.ts index 890928b5e..0a934eb39 100644 --- a/test/scenarios/tools/skills/typescript/src/index.ts +++ b/test/scenarios/tools/skills/typescript/src/index.ts @@ -1,13 +1,11 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; import path from "path"; import { fileURLToPath } from "url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const skillsDir = path.resolve(__dirname, "../../sample-skills"); diff --git a/test/scenarios/tools/tool-filtering/csharp/Program.cs b/test/scenarios/tools/tool-filtering/csharp/Program.cs index f3eae076d..23f6bf4b4 100644 --- a/test/scenarios/tools/tool-filtering/csharp/Program.cs +++ b/test/scenarios/tools/tool-filtering/csharp/Program.cs @@ -1,9 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/tool-filtering/go/main.go b/test/scenarios/tools/tool-filtering/go/main.go index abe3cc6e4..646582bdf 100644 --- a/test/scenarios/tools/tool-filtering/go/main.go +++ b/test/scenarios/tools/tool-filtering/go/main.go @@ -11,7 +11,7 @@ import ( const systemPrompt = `You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.` func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/tools/tool-filtering/typescript/src/index.ts b/test/scenarios/tools/tool-filtering/typescript/src/index.ts index b87b52f1e..7ab2d2f93 100644 --- a/test/scenarios/tools/tool-filtering/typescript/src/index.ts +++ b/test/scenarios/tools/tool-filtering/typescript/src/index.ts @@ -1,16 +1,15 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient } from "@github/copilot-sdk"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ model: "claude-haiku-4.5", systemMessage: { mode: "replace", - content: "You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.", + content: + "You are a helpful assistant. You have access to a limited set of tools. When asked about your tools, list exactly which tools you have available.", }, availableTools: ["grep", "glob", "view"], }); diff --git a/test/scenarios/tools/tool-overrides/csharp/Program.cs b/test/scenarios/tools/tool-overrides/csharp/Program.cs index a3b0d1318..c88b8dc2c 100644 --- a/test/scenarios/tools/tool-overrides/csharp/Program.cs +++ b/test/scenarios/tools/tool-overrides/csharp/Program.cs @@ -2,10 +2,7 @@ using GitHub.Copilot; using Microsoft.Extensions.AI; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/tool-overrides/go/main.go b/test/scenarios/tools/tool-overrides/go/main.go index 9a412c9d9..9f77fc56d 100644 --- a/test/scenarios/tools/tool-overrides/go/main.go +++ b/test/scenarios/tools/tool-overrides/go/main.go @@ -13,7 +13,7 @@ type GrepParams struct { } func main() { - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/tools/tool-overrides/typescript/src/index.ts b/test/scenarios/tools/tool-overrides/typescript/src/index.ts index bc1a26220..fa3fc457b 100644 --- a/test/scenarios/tools/tool-overrides/typescript/src/index.ts +++ b/test/scenarios/tools/tool-overrides/typescript/src/index.ts @@ -1,10 +1,8 @@ -import { CopilotClient, defineTool, approveAll , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient, defineTool, approveAll } from "@github/copilot-sdk"; import { z } from "zod"; async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -12,7 +10,8 @@ async function main() { onPermissionRequest: approveAll, tools: [ defineTool("grep", { - description: "A custom grep implementation that overrides the built-in", + description: + "A custom grep implementation that overrides the built-in", parameters: z.object({ query: z.string().describe("Search query"), }), diff --git a/test/scenarios/tools/virtual-filesystem/csharp/Program.cs b/test/scenarios/tools/virtual-filesystem/csharp/Program.cs index d967cd3e3..1081be345 100644 --- a/test/scenarios/tools/virtual-filesystem/csharp/Program.cs +++ b/test/scenarios/tools/virtual-filesystem/csharp/Program.cs @@ -5,10 +5,7 @@ // In-memory virtual filesystem var virtualFs = new Dictionary(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), -}); +using var client = new CopilotClient(); await client.StartAsync(); diff --git a/test/scenarios/tools/virtual-filesystem/go/main.go b/test/scenarios/tools/virtual-filesystem/go/main.go index faa315f0e..26737184c 100644 --- a/test/scenarios/tools/virtual-filesystem/go/main.go +++ b/test/scenarios/tools/virtual-filesystem/go/main.go @@ -72,7 +72,7 @@ func main() { }, } - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts index 7d0e0d040..432d91da9 100644 --- a/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts +++ b/test/scenarios/tools/virtual-filesystem/typescript/src/index.ts @@ -1,11 +1,12 @@ -import { CopilotClient, defineTool , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient, defineTool } from "@github/copilot-sdk"; import { z } from "zod"; // In-memory virtual filesystem const virtualFs = new Map(); const createFile = defineTool("create_file", { - description: "Create or overwrite a file at the given path with the provided content", + description: + "Create or overwrite a file at the given path with the provided content", parameters: z.object({ path: z.string().describe("File path"), content: z.string().describe("File content"), @@ -38,9 +39,7 @@ const listFiles = defineTool("list_files", { }); async function main() { - const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), - }); + const client = new CopilotClient(); try { const session = await client.createSession({ diff --git a/test/scenarios/transport/reconnect/typescript/src/index.ts b/test/scenarios/transport/reconnect/typescript/src/index.ts index 6fc1c417e..e1ea21fd1 100644 --- a/test/scenarios/transport/reconnect/typescript/src/index.ts +++ b/test/scenarios/transport/reconnect/typescript/src/index.ts @@ -2,7 +2,9 @@ import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - connection: RuntimeConnection.forUri(process.env.COPILOT_CLI_URL || "localhost:3000"), + connection: RuntimeConnection.forUri( + process.env.COPILOT_CLI_URL || "localhost:3000", + ), }); try { @@ -42,7 +44,9 @@ async function main() { await session2.disconnect(); console.log("Session 2 disconnected"); - console.log("\nReconnect test passed — both sessions completed successfully"); + console.log( + "\nReconnect test passed — both sessions completed successfully", + ); } finally { await client.stop(); } diff --git a/test/scenarios/transport/stdio/go/main.go b/test/scenarios/transport/stdio/go/main.go index c7716df2c..51b592431 100644 --- a/test/scenarios/transport/stdio/go/main.go +++ b/test/scenarios/transport/stdio/go/main.go @@ -10,7 +10,7 @@ import ( func main() { // Go SDK auto-reads COPILOT_CLI_PATH from env - client := copilot.NewClient(&copilot.ClientOptions{}) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { diff --git a/test/scenarios/transport/stdio/typescript/src/index.ts b/test/scenarios/transport/stdio/typescript/src/index.ts index 38f937d59..7df9cd888 100644 --- a/test/scenarios/transport/stdio/typescript/src/index.ts +++ b/test/scenarios/transport/stdio/typescript/src/index.ts @@ -1,8 +1,10 @@ -import { CopilotClient , RuntimeConnection } from "@github/copilot-sdk"; +import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - connection: RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }), + connection: RuntimeConnection.forStdio({ + path: process.env.COPILOT_CLI_PATH, + }), }); try { diff --git a/test/scenarios/transport/tcp/typescript/src/index.ts b/test/scenarios/transport/tcp/typescript/src/index.ts index e4775f545..9b6efd277 100644 --- a/test/scenarios/transport/tcp/typescript/src/index.ts +++ b/test/scenarios/transport/tcp/typescript/src/index.ts @@ -2,7 +2,9 @@ import { CopilotClient, RuntimeConnection } from "@github/copilot-sdk"; async function main() { const client = new CopilotClient({ - connection: RuntimeConnection.forUri(process.env.COPILOT_CLI_URL || "localhost:3000"), + connection: RuntimeConnection.forUri( + process.env.COPILOT_CLI_URL || "localhost:3000", + ), }); try { From 85f17805f5c0627d29d28330efa649c4f07ac64a Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 15:14:06 +0100 Subject: [PATCH 04/14] Address 4 post-merge review comments on PR #1376 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. docs/getting-started.md: Python snippet still used the removed `CopilotClientOptions` wrapper. Switch to the flat-kwargs form (`CopilotClient(connection=RuntimeConnection.for_uri(...))`). 2. python/README.md custom-permission-handler example imported per-kind variants (`PermissionRequestShell`, `PermissionDecisionApproveOnce`, `PermissionDecisionReject`) from `copilot` — but only `PermissionRequest` and `PermissionRequestResult` are re-exported at the top level. Fix the example to import the variant classes from `copilot.generated.session_events`. 3. python/copilot/client.py `RuntimeConnection` docstring referenced the old factory names `stdio`/`tcp`/`uri`. Update to `for_stdio`/`for_tcp`/`for_uri` to match the renamed methods. 4. python/copilot/session.py comment above `PermissionRequestResult` told users to construct variants with `kind=...` arguments — but Phase L baked the discriminator into a `ClassVar` default, so the generated variants reject `kind=` at the call site. Update the comment to reflect the new ergonomics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/getting-started.md | 6 ++---- python/README.md | 5 ++--- python/copilot/client.py | 7 ++++--- python/copilot/session.py | 5 +++-- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index f451fa69c..3b6178d92 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1967,12 +1967,10 @@ const session = await client.createSession({ onPermissionRequest: approveAll }); Python ```python -from copilot import CopilotClient, CopilotClientOptions, RuntimeConnection +from copilot import CopilotClient, RuntimeConnection from copilot.session import PermissionHandler -client = CopilotClient(CopilotClientOptions( - connection=RuntimeConnection.for_uri("localhost:4321"), -)) +client = CopilotClient(connection=RuntimeConnection.for_uri("localhost:4321")) await client.start() # Use the client normally diff --git a/python/README.md b/python/README.md index 4e415e320..38cbf40f3 100644 --- a/python/README.md +++ b/python/README.md @@ -574,11 +574,10 @@ session = await client.create_session( Provide your own function to inspect each request and apply custom logic (sync or async): ```python -from copilot import ( +from copilot import PermissionRequest, PermissionRequestResult +from copilot.generated.session_events import ( PermissionDecisionApproveOnce, PermissionDecisionReject, - PermissionRequest, - PermissionRequestResult, PermissionRequestShell, ) diff --git a/python/copilot/client.py b/python/copilot/client.py index 7e381dce9..b2f0b6d22 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -159,9 +159,10 @@ class TelemetryConfig(TypedDict, total=False): class RuntimeConnection: """Discriminated config describing how to reach the Copilot runtime. - Construct via the static factories :meth:`stdio`, :meth:`tcp`, or - :meth:`uri`. Each factory returns the matching subclass; pattern-match - on the subclass (or :func:`isinstance`) to branch on the transport. + Construct via the static factories :meth:`for_stdio`, :meth:`for_tcp`, + or :meth:`for_uri`. Each factory returns the matching subclass; + pattern-match on the subclass (or :func:`isinstance`) to branch on the + transport. Example: >>> CopilotClient() # default: stdio with the bundled runtime diff --git a/python/copilot/session.py b/python/copilot/session.py index 9798835e9..df5c18687 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -250,8 +250,9 @@ class PermissionNoResult: # The decision returned by a permission handler. Identical shape to the wire # ``PermissionDecision`` discriminated union, plus a :class:`PermissionNoResult` # sentinel for v1 servers. Construct via the generated variant classes: -# ``PermissionDecisionApproveOnce(kind=...)``, ``PermissionDecisionReject(kind=..., -# feedback=...)``, etc. +# ``PermissionDecisionApproveOnce()``, ``PermissionDecisionReject(feedback=...)``, +# etc. The ``kind`` discriminator is baked in as a ``ClassVar`` default by +# codegen, so callers must not pass it. PermissionRequestResult = PermissionDecision | PermissionNoResult From a95b69ae134ff4713edc29be47ee45b7560046a3 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 15:56:12 +0100 Subject: [PATCH 05/14] Replace hand-written PermissionRequestResult with PermissionDecision (.NET, Go) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python (#1376) drove out its own hand-written ``PermissionRequestResult`` wrapper in favour of the generated discriminated ``PermissionDecision`` union plus a small ``PermissionNoResult`` sentinel. This commit lands the same refactor for the .NET and Go SDKs. **.NET** The old ``PermissionRequestResult`` struct (just ``Kind`` + optional ``Feedback``) collapsed every decision variant into a flat string-tagged DTO, losing the rich per-variant payloads — ``Feedback`` on rejection, per-kind ``Approval`` lists on ``ApproveForSession`` / ``ApproveForLocation``, ``Domain`` on ``ApprovePermanently``, etc. The generated ``PermissionDecision`` (``Rpc.cs:4760``) is already a proper polymorphic hierarchy with ``[JsonDerivedType]`` wired up for every variant. Switch ``OnPermissionRequest`` to return ``Task`` and route the variant straight onto the wire — StreamJsonRpc handles the discriminator via the existing attributes. Additions: - ``PermissionDecisionNoResult`` — hand-written subclass of ``PermissionDecision`` used when the handler declines to respond so another connected client can answer. The SDK suppresses the wire response when it sees this variant. - Static factories on ``PermissionDecision`` for discoverability: ``ApproveOnce()``, ``Reject(feedback)``, ``UserNotAvailable()``, ``NoResult()``. For richer decisions that need an ``Approval`` payload, instantiate the variant class directly. Deletions: - ``PermissionRequestResult`` class (``Types.cs``) - ``PermissionRequestResultKind`` struct + obsolete enum-like wrappers - ``PermissionRequestResultKindTests.cs`` - ``PermissionRequestResult`` JSON serialization metadata **Go** Same shape: drop ``PermissionRequestResult`` + ``PermissionRequestResultKind`` in favour of ``rpc.PermissionDecision`` (already a sealed interface implemented by every variant). Added ``rpc.PermissionDecisionNoResult`` as a hand-written variant that satisfies the unexported ``permissionDecision()`` method — kept inside the ``rpc`` package since the sealing method is unexported. Handler signature changes from ``(PermissionRequestResult, error)`` to ``(rpc.PermissionDecision, error)``. ``PermissionHandler.ApproveAll`` now returns ``&rpc.PermissionDecisionApproveOnce{}``. Removed the ``rpcPermissionDecisionFromKind`` helper used to convert the flat kind back to a variant — no longer needed when the handler already returns the variant directly. **Tests / scenarios** All E2E tests and scenarios across .NET and Go updated to construct ``rpc.PermissionDecision*`` variants directly. The Go interface return type means explicit ``Task.FromResult(...)`` casts are needed in C# where lambdas previously inferred the wrapper type. **Doc style** Per repo convention, public docs do not reference protocol v1/v2 or internal transport details. The README copy for each SDK describes the behavioural semantics ("decline to respond so another client can answer") rather than the wire mechanism. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/README.md | 46 ++---- dotnet/src/Client.cs | 23 ++- dotnet/src/PermissionDecision.cs | 46 ++++++ dotnet/src/PermissionHandlers.cs | 6 +- dotnet/src/Session.cs | 25 +--- dotnet/src/Types.cs | 109 +------------- dotnet/test/E2E/MultiClientE2ETests.cs | 15 +- dotnet/test/E2E/PendingWorkResumeE2ETests.cs | 15 +- dotnet/test/E2E/PermissionE2ETests.cs | 26 ++-- dotnet/test/E2E/SuspendE2ETests.cs | 8 +- dotnet/test/E2E/ToolsE2ETests.cs | 7 +- .../Unit/PermissionRequestResultKindTests.cs | 140 ------------------ dotnet/test/Unit/SerializationTests.cs | 7 +- go/README.md | 51 +++---- go/client.go | 27 ++-- go/internal/e2e/multi_client_e2e_test.go | 17 ++- .../e2e/pending_work_resume_e2e_test.go | 10 +- go/internal/e2e/permissions_e2e_test.go | 54 +++---- go/internal/e2e/suspend_e2e_test.go | 7 +- go/internal/e2e/tools_e2e_test.go | 13 +- go/permissions.go | 8 +- go/rpc/permission_decision_no_result.go | 26 ++++ go/session.go | 22 +-- go/session_test.go | 20 --- go/types.go | 47 ++---- go/types_test.go | 90 ----------- .../callbacks/hooks/csharp/Program.cs | 3 +- test/scenarios/callbacks/hooks/go/main.go | 5 +- .../callbacks/permissions/csharp/Program.cs | 3 +- .../callbacks/permissions/go/main.go | 5 +- .../callbacks/user-input/csharp/Program.cs | 3 +- .../scenarios/callbacks/user-input/go/main.go | 5 +- test/scenarios/tools/skills/csharp/Program.cs | 3 +- test/scenarios/tools/skills/go/main.go | 5 +- .../virtual-filesystem/csharp/Program.cs | 3 +- .../tools/virtual-filesystem/go/main.go | 5 +- 36 files changed, 274 insertions(+), 631 deletions(-) create mode 100644 dotnet/src/PermissionDecision.cs delete mode 100644 dotnet/test/Unit/PermissionRequestResultKindTests.cs create mode 100644 go/rpc/permission_decision_no_result.go diff --git a/dotnet/README.md b/dotnet/README.md index f01d87474..372b95a33 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -749,7 +749,7 @@ var session = await client.CreateSessionAsync(new SessionConfig ### Custom Permission Handler -Provide your own permission handler (`Func>`) to inspect each request and apply custom logic: +Provide your own permission handler (`Func>`) to inspect each request and apply custom logic: ```csharp var session = await client.CreateSessionAsync(new SessionConfig @@ -757,43 +757,29 @@ var session = await client.CreateSessionAsync(new SessionConfig Model = "gpt-5", OnPermissionRequest = async (request, invocation) => { - // request.Kind — string discriminator for the type of operation being requested: - // "shell" — executing a shell command - // "write" — writing or editing a file - // "read" — reading a file - // "mcp" — calling an MCP tool - // "custom_tool" — calling one of your registered tools - // "url" — fetching a URL - // "memory" — accessing or modifying assistant memory - // "hook" — invoking a registered hook - // request.ToolCallId — the tool call that triggered this request - // request.ToolName — name of the tool (for custom-tool / mcp) - // request.FileName — file being written (for write) - // request.FullCommandText — full shell command text (for shell) - - if (request.Kind == "shell") + // Pattern-match on the discriminated PermissionRequest union to access + // per-kind fields (FullCommandText, Path, ToolName, …). + return request switch { - // Deny shell commands - return new PermissionRequestResult { Kind = PermissionRequestResultKind.Rejected }; - } - - return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; + PermissionRequestShell s => PermissionDecision.Reject($"Refusing shell: {s.FullCommandText}"), + _ => PermissionDecision.ApproveOnce(), + }; } }); ``` -### Permission Result Kinds +### Permission Decisions -The `Kind` property must be one of the canonical `PermissionRequestResultKind` values. Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on `permission.completed` session events. +The handler returns a `PermissionDecision`. Use the static factories for common cases (returned types are the strongly-typed variant classes — full IntelliSense via `PermissionDecision.`): -| Value | Wire value | Meaning | -| ------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `PermissionRequestResultKind.Approved` | `"approve-once"` | Allow this single request | -| `PermissionRequestResultKind.Rejected` | `"reject"` | Deny the request | -| `PermissionRequestResultKind.UserNotAvailable` | `"user-not-available"` | Deny the request because no user is available to confirm it | -| `PermissionRequestResultKind.NoResult` | `"no-result"` | Leave the permission request unanswered (the SDK returns without calling the RPC). Not allowed for protocol v2 permission requests (will be rejected). | +| Factory | Meaning | +| -------------------------------------- | -------------------------------------------------------------------------------------------- | +| `PermissionDecision.ApproveOnce()` | Allow this single request | +| `PermissionDecision.Reject(feedback)` | Deny the request, optionally forwarding feedback to the LLM | +| `PermissionDecision.UserNotAvailable()`| Deny the request because no user is available to confirm it | +| `PermissionDecision.NoResult()` | Decline to respond, allowing another connected client to answer instead | -> The past-tense names `PermissionRequestResultKind.DeniedInteractivelyByUser`, `PermissionRequestResultKind.DeniedCouldNotRequestFromUser`, and `PermissionRequestResultKind.DeniedByRules` remain as `[Obsolete]` aliases for backward compatibility — prefer the canonical members above in new code. +For richer decisions that need an `Approval` payload — `PermissionDecisionApproveForSession`, `PermissionDecisionApproveForLocation`, `PermissionDecisionApprovePermanently` — instantiate the variant class directly. ### Resuming Sessions diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 560d8c0b3..268381618 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -53,8 +53,8 @@ namespace GitHub.Copilot; /// public sealed partial class CopilotClient : IDisposable, IAsyncDisposable { - internal const string NoResultPermissionV2ErrorMessage = - "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."; + internal const string NoResultPermissionDirectRpcErrorMessage = + "Permission handlers cannot return PermissionDecision.NoResult() for this permission request."; /// /// Minimum protocol version this SDK can communicate with. @@ -1885,23 +1885,20 @@ public async ValueTask OnPermissionRequestV2(string try { - var result = await session.HandlePermissionRequestAsync(permissionRequest); - if (result.Kind == new PermissionRequestResultKind("no-result")) + var decision = await session.HandlePermissionRequestAsync(permissionRequest); + if (decision is PermissionDecisionNoResult) { - throw new InvalidOperationException(NoResultPermissionV2ErrorMessage); + throw new InvalidOperationException(NoResultPermissionDirectRpcErrorMessage); } - return new PermissionRequestResponseV2(result); + return new PermissionRequestResponseV2(decision); } - catch (InvalidOperationException ex) when (ex.Message == NoResultPermissionV2ErrorMessage) + catch (InvalidOperationException ex) when (ex.Message == NoResultPermissionDirectRpcErrorMessage) { throw; } catch (Exception) { - return new PermissionRequestResponseV2(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.UserNotAvailable - }); + return new PermissionRequestResponseV2(PermissionDecision.UserNotAvailable()); } } } @@ -2079,7 +2076,7 @@ internal record ToolCallResponseV2( ToolResultObject Result); internal record PermissionRequestResponseV2( - PermissionRequestResult Result); + PermissionDecision Result); [JsonSourceGenerationOptions( JsonSerializerDefaults.Web, @@ -2103,8 +2100,6 @@ internal record PermissionRequestResponseV2( [JsonSerializable(typeof(GetSessionMetadataRequest))] [JsonSerializable(typeof(GetSessionMetadataResponse))] [JsonSerializable(typeof(ModelCapabilitiesOverride))] - [JsonSerializable(typeof(PermissionRequestResult))] - [JsonSerializable(typeof(PermissionRequestResultKind))] [JsonSerializable(typeof(PermissionRequestResponseV2))] [JsonSerializable(typeof(ProviderConfig))] [JsonSerializable(typeof(ResumeSessionRequest))] diff --git a/dotnet/src/PermissionDecision.cs b/dotnet/src/PermissionDecision.cs new file mode 100644 index 000000000..bc447ad87 --- /dev/null +++ b/dotnet/src/PermissionDecision.cs @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using System.Text.Json.Serialization; + +namespace GitHub.Copilot.Rpc; + +/// +/// SDK-only value indicating the handler +/// declines to respond to this permission request. The SDK then suppresses +/// the response so another connected client can answer instead. +/// +public sealed class PermissionDecisionNoResult : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "no-result"; +} + +/// +/// Static factories for the common variants +/// returned by OnPermissionRequest handlers. Use these for quick +/// discoverability via PermissionDecision.<dot>. For richer +/// decisions (per-session, per-location, permanent) that need an +/// Approval payload, instantiate the variant class directly. +/// +[JsonDerivedType(typeof(PermissionDecisionNoResult), "no-result")] +public partial class PermissionDecision +{ + /// Approve this single request. + public static PermissionDecisionApproveOnce ApproveOnce() => new(); + + /// Reject the request, optionally forwarding feedback to the LLM. + public static PermissionDecisionReject Reject(string? feedback = null) => + new() { Feedback = feedback }; + + /// Deny the request because no user is available to confirm it. + public static PermissionDecisionUserNotAvailable UserNotAvailable() => new(); + + /// + /// Decline to respond to this permission request, allowing another + /// connected client to answer instead. + /// + public static PermissionDecisionNoResult NoResult() => new(); +} diff --git a/dotnet/src/PermissionHandlers.cs b/dotnet/src/PermissionHandlers.cs index 0e4af7eac..4386e8ba6 100644 --- a/dotnet/src/PermissionHandlers.cs +++ b/dotnet/src/PermissionHandlers.cs @@ -2,12 +2,14 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; + namespace GitHub.Copilot; /// Provides pre-built permission request handlers. public static class PermissionHandler { /// A permission handler that approves all permission requests. - public static Func> ApproveAll { get; } = - (_, _) => Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + public static Func> ApproveAll { get; } = + (_, _) => Task.FromResult(PermissionDecision.ApproveOnce()); } diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 0916a7b21..9af36f535 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -60,7 +60,7 @@ public sealed partial class CopilotSession : IAsyncDisposable private readonly ILogger _logger; private readonly CopilotClient _parentClient; - private volatile Func>? _permissionHandler; + private volatile Func>? _permissionHandler; private volatile Func>? _userInputHandler; private volatile Func>? _elicitationHandler; private volatile Func>? _exitPlanModeHandler; @@ -535,7 +535,7 @@ internal void RegisterTools(ICollection tools) /// When the assistant needs permission to perform certain actions (e.g., file operations), /// this handler is called to approve or deny the request. /// - internal void RegisterPermissionHandler(Func>? handler) + internal void RegisterPermissionHandler(Func>? handler) { _permissionHandler = handler; } @@ -545,16 +545,13 @@ internal void RegisterPermissionHandler(Func /// The permission request data from the CLI. /// A task that resolves with the permission decision. - internal async Task HandlePermissionRequestAsync(JsonElement permissionRequestData) + internal async Task HandlePermissionRequestAsync(JsonElement permissionRequestData) { var handler = _permissionHandler; if (handler == null) { - return new PermissionRequestResult - { - Kind = PermissionRequestResultKind.UserNotAvailable - }; + return PermissionDecision.UserNotAvailable(); } var request = JsonSerializer.Deserialize(permissionRequestData.GetRawText(), SessionEventsJsonContext.Default.PermissionRequest) @@ -765,7 +762,7 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName, /// /// Executes a permission handler and sends the result back via the HandlePendingPermissionRequest RPC. /// - private async Task ExecutePermissionAndRespondAsync(string requestId, PermissionRequest permissionRequest, Func> handler) + private async Task ExecutePermissionAndRespondAsync(string requestId, PermissionRequest permissionRequest, Func> handler) { try { @@ -775,20 +772,17 @@ private async Task ExecutePermissionAndRespondAsync(string requestId, Permission }; var permissionTimestamp = Stopwatch.GetTimestamp(); - var result = await handler(permissionRequest, invocation); + var decision = await handler(permissionRequest, invocation); LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, "CopilotSession.ExecutePermissionAndRespondAsync dispatch. Elapsed={Elapsed}, SessionId={SessionId}, RequestId={RequestId}", permissionTimestamp, SessionId, requestId); - if (result.Kind == new PermissionRequestResultKind("no-result")) + if (decision is PermissionDecisionNoResult) { return; } var responseRpcTimestamp = Stopwatch.GetTimestamp(); - PermissionDecision decision = result.Kind == PermissionRequestResultKind.Rejected - ? new PermissionDecisionReject { Feedback = result.Feedback } - : new PermissionDecision { Kind = result.Kind.Value }; await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, decision); LoggingHelpers.LogTiming(_logger, LogLevel.Debug, null, "CopilotSession.ExecutePermissionAndRespondAsync response sent successfully. Elapsed={Elapsed}, SessionId={SessionId}, RequestId={RequestId}", @@ -800,10 +794,7 @@ private async Task ExecutePermissionAndRespondAsync(string requestId, Permission { try { - await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, new PermissionDecision - { - Kind = PermissionRequestResultKind.UserNotAvailable.Value - }); + await Rpc.Permissions.HandlePendingPermissionRequestAsync(requestId, PermissionDecision.UserNotAvailable()); } catch (IOException) { diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index bb72820e5..c83415222 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -675,111 +675,6 @@ public sealed class ToolInvocation public object? Arguments { get; set; } } -/// Describes the kind of a permission request result. -[JsonConverter(typeof(PermissionRequestResultKind.Converter))] -[DebuggerDisplay("{Value,nq}")] -public readonly struct PermissionRequestResultKind : IEquatable -{ - /// Gets the kind indicating the permission was approved for this one instance. - public static PermissionRequestResultKind Approved { get; } = new("approve-once"); - - /// Gets the kind indicating the permission was denied interactively by the user. - public static PermissionRequestResultKind Rejected { get; } = new("reject"); - - /// Gets the kind indicating the permission was denied because user confirmation was unavailable. - public static PermissionRequestResultKind UserNotAvailable { get; } = new("user-not-available"); - - /// Gets the kind indicating no permission decision was made. - public static PermissionRequestResultKind NoResult { get; } = new("no-result"); - - /// Gets the underlying string value of this . - public string Value => _value ?? string.Empty; - - private readonly string? _value; - - /// Initializes a new instance of the struct. - /// The string value for this kind. - [JsonConstructor] - public PermissionRequestResultKind(string value) => _value = value; - - /// - public static bool operator ==(PermissionRequestResultKind left, PermissionRequestResultKind right) => left.Equals(right); - - /// - public static bool operator !=(PermissionRequestResultKind left, PermissionRequestResultKind right) => !left.Equals(right); - - /// - public override bool Equals([NotNullWhen(true)] object? obj) => obj is PermissionRequestResultKind other && Equals(other); - - /// - public bool Equals(PermissionRequestResultKind other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); - - /// - public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); - - /// - public override string ToString() => Value; - - /// Provides a for serializing instances. - [EditorBrowsable(EditorBrowsableState.Never)] - public sealed class Converter : JsonConverter - { - /// - public override PermissionRequestResultKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.String) - { - throw new JsonException("Expected string for PermissionRequestResultKind."); - } - - var value = reader.GetString(); - if (value is null) - { - throw new JsonException("PermissionRequestResultKind value cannot be null."); - } - - return new PermissionRequestResultKind(value); - } - - /// - public override void Write(Utf8JsonWriter writer, PermissionRequestResultKind value, JsonSerializerOptions options) => - writer.WriteStringValue(value.Value); - } -} - -/// -/// Result of a permission request evaluation. -/// -public sealed class PermissionRequestResult -{ - /// - /// Permission decision kind. Construct values with the static members on - /// : - /// - /// — allow this single request. - /// — deny the request. - /// — deny because no user is available to confirm. - /// — leave the pending request unanswered (protocol v1 only; rejected by protocol v2 servers). - /// - /// - [JsonPropertyName("kind")] - public PermissionRequestResultKind Kind { get; set; } - - /// - /// Permission rules to apply for the decision. - /// - [JsonPropertyName("rules")] - public IList? Rules { get; set; } - - /// - /// Optional human-readable feedback to forward to the LLM along with the - /// decision. Mirrors the feedback field on the RPC-level - /// type. - /// - [JsonPropertyName("feedback")] - public string? Feedback { get; set; } -} - /// /// Contains context for a permission request callback. /// @@ -2333,7 +2228,7 @@ protected SessionConfigBase(SessionConfigBase? other) public bool? EnableSessionTelemetry { get; set; } /// Handler for permission requests from the server. - public Func>? OnPermissionRequest { get; set; } + public Func>? OnPermissionRequest { get; set; } /// Handler for user input requests from the agent. public Func>? OnUserInputRequest { get; set; } @@ -3049,8 +2944,6 @@ public sealed class SystemMessageTransformRpcResponse [JsonSerializable(typeof(ModelPolicy))] [JsonSerializable(typeof(ModelSupports))] [JsonSerializable(typeof(ModelVisionLimits))] -[JsonSerializable(typeof(PermissionRequestResult))] -[JsonSerializable(typeof(PermissionRequestResultKind))] [JsonSerializable(typeof(PingRequest))] [JsonSerializable(typeof(PingResponse))] [JsonSerializable(typeof(ProviderConfig))] diff --git a/dotnet/test/E2E/MultiClientE2ETests.cs b/dotnet/test/E2E/MultiClientE2ETests.cs index 34efd09b2..faaf38393 100644 --- a/dotnet/test/E2E/MultiClientE2ETests.cs +++ b/dotnet/test/E2E/MultiClientE2ETests.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using GitHub.Copilot.Test.Harness; using Microsoft.Extensions.AI; using System.Collections.Concurrent; @@ -152,17 +153,14 @@ public async Task One_Client_Approves_Permission_And_Both_See_The_Result() OnPermissionRequest = (request, _) => { client1PermissionRequests.Add(request); - return Task.FromResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.Approved, - }); + return Task.FromResult(PermissionDecision.ApproveOnce()); }, }); // Client 2 resumes — its handler never completes, so only client 1's approval takes effect var session2 = await Client2.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig { - OnPermissionRequest = (_, _) => new TaskCompletionSource().Task, + OnPermissionRequest = (_, _) => new TaskCompletionSource().Task, }); var client1Events = new ConcurrentBag(); @@ -204,16 +202,13 @@ public async Task One_Client_Rejects_Permission_And_Both_See_The_Result() { var session1 = await Client1.CreateSessionAsync(new SessionConfig { - OnPermissionRequest = (_, _) => Task.FromResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.Rejected, - }), + OnPermissionRequest = (_, _) => Task.FromResult(PermissionDecision.Reject()), }); // Client 2 resumes — its handler never completes var session2 = await Client2.ResumeSessionAsync(session1.SessionId, new ResumeSessionConfig { - OnPermissionRequest = (_, _) => new TaskCompletionSource().Task, + OnPermissionRequest = (_, _) => new TaskCompletionSource().Task, }); var client1Events = new ConcurrentBag(); diff --git a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs index 889dc0050..eee59690c 100644 --- a/dotnet/test/E2E/PendingWorkResumeE2ETests.cs +++ b/dotnet/test/E2E/PendingWorkResumeE2ETests.cs @@ -1,7 +1,8 @@ -/*--------------------------------------------------------------------------------------------- +/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using GitHub.Copilot.Test.Harness; using Microsoft.Extensions.AI; using System.ComponentModel; @@ -21,7 +22,7 @@ public class PendingWorkResumeE2ETests(E2ETestFixture fixture, ITestOutputHelper public async Task Should_Continue_Pending_Permission_Request_After_Resume() { var originalPermissionRequest = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var releaseOriginalPermission = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseOriginalPermission = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var resumedToolInvoked = false; await using var server = Ctx.CreateClient(options: new CopilotClientOptions { Connection = RuntimeConnection.ForTcp(connectionToken: SharedToken) }); @@ -59,10 +60,7 @@ await session1.SendAsync(new MessageOptions var session2 = await resumedTcpClient.ResumeSessionAsync(sessionId, new ResumeSessionConfig { ContinuePendingWork = true, - OnPermissionRequest = (_, _) => Task.FromResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.NoResult - }), + OnPermissionRequest = (_, _) => Task.FromResult(PermissionDecision.NoResult()), Tools = [ AIFunctionFactory.Create( @@ -90,10 +88,7 @@ await session1.SendAsync(new MessageOptions } finally { - releaseOriginalPermission.TrySetResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.UserNotAvailable, - }); + releaseOriginalPermission.TrySetResult(PermissionDecision.UserNotAvailable()); } [Description("Transforms a value after permission is granted")] diff --git a/dotnet/test/E2E/PermissionE2ETests.cs b/dotnet/test/E2E/PermissionE2ETests.cs index 953ab1469..d25c3c929 100644 --- a/dotnet/test/E2E/PermissionE2ETests.cs +++ b/dotnet/test/E2E/PermissionE2ETests.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using GitHub.Copilot.Test.Harness; using Microsoft.Extensions.AI; using System.Text.Json; @@ -43,7 +44,7 @@ public async Task Should_Invoke_Permission_Handler_For_Write_Operations() { writePermissionRequestReceived.TrySetResult(writeRequest); } - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); } }); @@ -86,10 +87,7 @@ public async Task Should_Deny_Permission_When_Handler_Returns_Denied() { OnPermissionRequest = (request, invocation) => { - return Task.FromResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.Rejected - }); + return Task.FromResult(PermissionDecision.Reject()); } }); @@ -135,7 +133,7 @@ public async Task Should_Deny_Tool_Operations_When_Handler_Explicitly_Denies() var session = await CreateSessionAsync(new SessionConfig { OnPermissionRequest = (_, _) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.UserNotAvailable }) + Task.FromResult(PermissionDecision.UserNotAvailable()) }); var permissionDenied = false; @@ -181,7 +179,7 @@ public async Task Should_Handle_Async_Permission_Handler() { permissionRequestReceived = true; await Task.Yield(); - return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; + return PermissionDecision.ApproveOnce(); } }); @@ -212,7 +210,7 @@ public async Task Should_Resume_Session_With_Permission_Handler() OnPermissionRequest = (request, invocation) => { permissionRequestReceived = true; - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); } }); @@ -262,7 +260,7 @@ public async Task Should_Deny_Tool_Operations_When_Handler_Explicitly_Denies_Aft var session2 = await Client.ResumeSessionAsync(sessionId, new ResumeSessionConfig { OnPermissionRequest = (_, _) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.UserNotAvailable }) + Task.FromResult(PermissionDecision.UserNotAvailable()) }); var permissionDenied = false; @@ -297,7 +295,7 @@ public async Task Should_Receive_ToolCallId_In_Permission_Requests() { receivedToolCallId = true; } - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); } }); @@ -340,7 +338,7 @@ void AddLifecycleEvent(string phase, string? toolCallId) handlerEntered.TrySetResult(); await releaseHandler.Task.WaitAsync(TimeSpan.FromSeconds(30)); AddLifecycleEvent("permission-complete", shellRequest.ToolCallId); - return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; + return PermissionDecision.ApproveOnce(); } }); @@ -438,7 +436,7 @@ public async Task Should_Handle_Concurrent_Permission_Requests_From_Parallel_Too } await bothPermissionRequestsStarted.Task.WaitAsync(TimeSpan.FromSeconds(30)); - return new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; + return PermissionDecision.ApproveOnce(); } }); @@ -515,7 +513,7 @@ public async Task Should_Deny_Permission_With_NoResult_Kind() OnPermissionRequest = (_, _) => { permissionCalled.TrySetResult(true); - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.NoResult }); + return Task.FromResult(PermissionDecision.NoResult()); } }); @@ -541,7 +539,7 @@ public async Task Should_Short_Circuit_Permission_Handler_When_Set_Approve_All_E OnPermissionRequest = (_, _) => { Interlocked.Increment(ref handlerCallCount); - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); }, }); diff --git a/dotnet/test/E2E/SuspendE2ETests.cs b/dotnet/test/E2E/SuspendE2ETests.cs index d3aa8067d..f531f0e59 100644 --- a/dotnet/test/E2E/SuspendE2ETests.cs +++ b/dotnet/test/E2E/SuspendE2ETests.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using Microsoft.Extensions.AI; using System.ComponentModel; using Xunit; @@ -100,7 +101,7 @@ public async Task Should_Cancel_Pending_Permission_Request_When_Suspending() // and the underlying tool function is never invoked because the cancelled // permission means the runtime never grants execution. var permissionHandlerEntered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var releasePermissionHandler = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releasePermissionHandler = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var toolInvoked = false; var session = await CreateSessionAsync(new SessionConfig @@ -142,10 +143,7 @@ public async Task Should_Cancel_Pending_Permission_Request_When_Suspending() { // Defensive: release the dangling SDK-side handler task so it doesn't keep // a stray TaskCompletionSource alive after the test ends. - releasePermissionHandler.TrySetResult(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.UserNotAvailable, - }); + releasePermissionHandler.TrySetResult(PermissionDecision.UserNotAvailable()); } await session.DisposeAsync(); diff --git a/dotnet/test/E2E/ToolsE2ETests.cs b/dotnet/test/E2E/ToolsE2ETests.cs index c36bf2294..57ed6be2d 100644 --- a/dotnet/test/E2E/ToolsE2ETests.cs +++ b/dotnet/test/E2E/ToolsE2ETests.cs @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ +using GitHub.Copilot.Rpc; using GitHub.Copilot.Test.Harness; using Microsoft.Extensions.AI; using System.Collections.ObjectModel; @@ -201,7 +202,7 @@ static string SafeLookup([Description("Lookup ID")] string id) OnPermissionRequest = (_, _) => { didRunPermissionRequest = true; - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.NoResult }); + return Task.FromResult(PermissionDecision.NoResult()); } }); @@ -258,7 +259,7 @@ public async Task Invokes_Custom_Tool_With_Permission_Handler() OnPermissionRequest = (request, invocation) => { permissionRequests.Add(request); - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); }, }); @@ -289,7 +290,7 @@ public async Task Denies_Custom_Tool_When_Permission_Denied() var session = await Client.CreateSessionAsync(new SessionConfig { Tools = [AIFunctionFactory.Create(EncryptStringDenied, "encrypt_string")], - OnPermissionRequest = async (request, invocation) => new() { Kind = PermissionRequestResultKind.Rejected }, + OnPermissionRequest = async (request, invocation) => PermissionDecision.Reject(), }); await session.SendAsync(new MessageOptions diff --git a/dotnet/test/Unit/PermissionRequestResultKindTests.cs b/dotnet/test/Unit/PermissionRequestResultKindTests.cs deleted file mode 100644 index ce828e6ef..000000000 --- a/dotnet/test/Unit/PermissionRequestResultKindTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -using System.Text.Json; -using Xunit; - -namespace GitHub.Copilot.Test.Unit; - -public class PermissionRequestResultKindTests -{ - private static readonly JsonSerializerOptions s_jsonOptions = new(JsonSerializerDefaults.Web) - { - TypeInfoResolver = TestJsonContext.Default, - }; - - [Fact] - public void WellKnownKinds_HaveExpectedValues() - { - Assert.Equal("approve-once", PermissionRequestResultKind.Approved.Value); - Assert.Equal("reject", PermissionRequestResultKind.Rejected.Value); - Assert.Equal("user-not-available", PermissionRequestResultKind.UserNotAvailable.Value); - Assert.Equal("no-result", PermissionRequestResultKind.NoResult.Value); - } - - [Fact] - public void Equals_SameValue_ReturnsTrue() - { - var a = new PermissionRequestResultKind("approve-once"); - Assert.True(a == PermissionRequestResultKind.Approved); - Assert.True(a.Equals(PermissionRequestResultKind.Approved)); - Assert.True(a.Equals((object)PermissionRequestResultKind.Approved)); - } - - [Fact] - public void Equals_DifferentValue_ReturnsFalse() - { - Assert.True(PermissionRequestResultKind.Approved != PermissionRequestResultKind.Rejected); - Assert.False(PermissionRequestResultKind.Approved.Equals(PermissionRequestResultKind.Rejected)); - } - - [Fact] - public void Equals_IsCaseInsensitive() - { - var upper = new PermissionRequestResultKind("APPROVE-ONCE"); - Assert.Equal(PermissionRequestResultKind.Approved, upper); - } - - [Fact] - public void GetHashCode_IsCaseInsensitive() - { - var upper = new PermissionRequestResultKind("APPROVE-ONCE"); - Assert.Equal(PermissionRequestResultKind.Approved.GetHashCode(), upper.GetHashCode()); - } - - [Fact] - public void ToString_ReturnsValue() - { - Assert.Equal("approve-once", PermissionRequestResultKind.Approved.ToString()); - Assert.Equal("reject", PermissionRequestResultKind.Rejected.ToString()); - } - - [Fact] - public void CustomValue_IsPreserved() - { - var custom = new PermissionRequestResultKind("custom-kind"); - Assert.Equal("custom-kind", custom.Value); - Assert.Equal("custom-kind", custom.ToString()); - } - - [Fact] - public void Constructor_NullValue_TreatedAsEmpty() - { - var kind = new PermissionRequestResultKind(null!); - Assert.Equal(string.Empty, kind.Value); - } - - [Fact] - public void Default_HasEmptyStringValue() - { - var defaultKind = default(PermissionRequestResultKind); - Assert.Equal(string.Empty, defaultKind.Value); - Assert.Equal(string.Empty, defaultKind.ToString()); - Assert.Equal(defaultKind.GetHashCode(), defaultKind.GetHashCode()); - } - - [Fact] - public void Equals_NonPermissionRequestResultKindObject_ReturnsFalse() - { - Assert.False(PermissionRequestResultKind.Approved.Equals("approve-once")); - } - - [Fact] - public void JsonSerialize_WritesStringValue() - { - var result = new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }; - var json = JsonSerializer.Serialize(result, s_jsonOptions); - Assert.Contains("\"kind\":\"approve-once\"", json); - } - - [Fact] - public void JsonDeserialize_ReadsStringValue() - { - var json = """{"kind":"reject"}"""; - var result = JsonSerializer.Deserialize(json, s_jsonOptions)!; - Assert.Equal(PermissionRequestResultKind.Rejected, result.Kind); - } - - [Fact] - public void JsonRoundTrip_PreservesAllKinds() - { - var kinds = new[] - { - PermissionRequestResultKind.Approved, - PermissionRequestResultKind.Rejected, - PermissionRequestResultKind.UserNotAvailable, - PermissionRequestResultKind.NoResult, - }; - - foreach (var kind in kinds) - { - var result = new PermissionRequestResult { Kind = kind }; - var json = JsonSerializer.Serialize(result, s_jsonOptions); - var deserialized = JsonSerializer.Deserialize(json, s_jsonOptions)!; - Assert.Equal(kind, deserialized.Kind); - } - } - - [Fact] - public void JsonRoundTrip_CustomValue() - { - var result = new PermissionRequestResult { Kind = new PermissionRequestResultKind("custom") }; - var json = JsonSerializer.Serialize(result, s_jsonOptions); - var deserialized = JsonSerializer.Deserialize(json, s_jsonOptions)!; - Assert.Equal("custom", deserialized.Kind.Value); - } -} - -[System.Text.Json.Serialization.JsonSerializable(typeof(PermissionRequestResult))] -internal partial class TestJsonContext : System.Text.Json.Serialization.JsonSerializerContext; diff --git a/dotnet/test/Unit/SerializationTests.cs b/dotnet/test/Unit/SerializationTests.cs index 6a3802d0c..a95bd7ce2 100644 --- a/dotnet/test/Unit/SerializationTests.cs +++ b/dotnet/test/Unit/SerializationTests.cs @@ -295,12 +295,9 @@ public void QueuedCommandResult_SerializesHandledAsBoolean_WithSdkOptions() public void PermissionDecision_SerializesBaseDiscriminator_WithSdkOptions() { var options = GetSerializerOptions(); - var original = new PermissionDecision - { - Kind = PermissionRequestResultKind.Approved.Value - }; + var original = PermissionDecision.ApproveOnce(); - var json = JsonSerializer.Serialize(original, options); + var json = JsonSerializer.Serialize(original, options); using var document = JsonDocument.Parse(json); Assert.Equal("approve-once", document.RootElement.GetProperty("kind").GetString()); diff --git a/go/README.md b/go/README.md index 8dcffb1a8..ca030c007 100644 --- a/go/README.md +++ b/go/README.md @@ -594,45 +594,38 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi Provide your own `PermissionHandlerFunc` to inspect each request and apply custom logic: ```go +import ( + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" +) + session, err := client.CreateSession(context.Background(), &copilot.SessionConfig{ Model: "gpt-5", - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - // request.Kind — what type of operation is being requested: - // copilot.KindShell — executing a shell command - // copilot.Write — writing or editing a file - // copilot.Read — reading a file - // copilot.MCP — calling an MCP tool - // copilot.CustomTool — calling one of your registered tools - // copilot.URL — fetching a URL - // copilot.Memory — accessing or updating Copilot-managed memory - // copilot.Hook — invoking a registered hook - // request.ToolCallID — pointer to the tool call that triggered this request - // request.ToolName — pointer to the name of the tool (for custom-tool / mcp) - // request.FileName — pointer to the file being written (for write) - // request.FullCommandText — pointer to the full shell command (for shell) - - if request.Kind == copilot.KindShell { - // Deny shell commands - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindRejected}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + // Type-switch on the discriminated PermissionRequest variants to + // access per-kind fields: + if shell, ok := request.(*copilot.PermissionRequestShell); ok { + return &rpc.PermissionDecisionReject{ + Feedback: pointer(fmt.Sprintf("Refusing shell: %s", shell.FullCommandText)), + }, nil } - - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) ``` -### Permission Result Kinds +### Permission Decisions -The `Kind` field must be one of the canonical `PermissionRequestResultKind` constants. Approval decisions are present-tense — they describe the decision to apply, not the past-tense outcome reported back on `permission.completed` session events. +The handler returns an `rpc.PermissionDecision` — a sealed interface implemented by every decision variant: -| Constant | Wire value | Meaning | -| --------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------------- | -| `PermissionRequestResultKindApproved` | `"approve-once"` | Allow this single request | -| `PermissionRequestResultKindRejected` | `"reject"` | Deny the request | -| `PermissionRequestResultKindUserNotAvailable` | `"user-not-available"` | Deny the request because no user is available to confirm it | -| `PermissionRequestResultKindNoResult` | `"no-result"` | Leave the permission request unanswered (protocol v1 only; rejected by protocol v2 servers) | +| Variant | Meaning | +| ---------------------------------------- | ---------------------------------------------------------------------------------- | +| `&rpc.PermissionDecisionApproveOnce{}` | Allow this single request | +| `&rpc.PermissionDecisionReject{...}` | Deny the request (set `Feedback` to forward a message to the LLM) | +| `&rpc.PermissionDecisionUserNotAvailable{}` | Deny because no user is available to confirm | +| `&rpc.PermissionDecisionNoResult{}` | Decline to respond, allowing another connected client to answer instead | -> The past-tense names `PermissionRequestResultKindDeniedInteractivelyByUser`, `PermissionRequestResultKindDeniedCouldNotRequestFromUser`, and `PermissionRequestResultKindDeniedByRules` remain as deprecated aliases for backward compatibility — prefer the canonical constants above in new code. +Richer decisions (`PermissionDecisionApproveForSession`, `PermissionDecisionApproveForLocation`, `PermissionDecisionApprovePermanently`) carry per-kind approval payloads — instantiate the variant struct directly. ### Resuming Sessions diff --git a/go/client.go b/go/client.go index e7ac2a9a1..046a4763f 100644 --- a/go/client.go +++ b/go/client.go @@ -52,7 +52,7 @@ import ( "github.com/github/copilot-sdk/go/rpc" ) -const noResultPermissionV2Error = "permission handlers cannot return 'no-result' when connected to a protocol v2 server" +const noResultPermissionDirectRpcError = "permission handlers cannot return PermissionDecisionNoResult for this permission request" func validateSessionFsConfig(config *SessionFsConfig) error { if config == nil { @@ -1930,7 +1930,7 @@ type permissionRequestV2 struct { // permissionResponseV2 is the v2 RPC response payload for permission.request. type permissionResponseV2 struct { - Result PermissionRequestResult `json:"result"` + Result rpc.PermissionDecision `json:"result"` } // handleToolCallRequestV2 handles a v2-style tool.call RPC request from the server. @@ -1994,28 +1994,23 @@ func (c *Client) handlePermissionRequestV2(req permissionRequestV2) (*permission handler := session.getPermissionHandler() if handler == nil { - return &permissionResponseV2{ - Result: PermissionRequestResult{ - Kind: PermissionRequestResultKindDeniedCouldNotRequestFromUser, - }, - }, nil + return &permissionResponseV2{Result: &rpc.PermissionDecisionUserNotAvailable{}}, nil } invocation := PermissionInvocation{ SessionID: session.SessionID, } - result, err := handler(req.Request, invocation) + decision, err := handler(req.Request, invocation) if err != nil { - return &permissionResponseV2{ - Result: PermissionRequestResult{ - Kind: PermissionRequestResultKindDeniedCouldNotRequestFromUser, - }, - }, nil + return &permissionResponseV2{Result: &rpc.PermissionDecisionUserNotAvailable{}}, nil } - if result.Kind == "no-result" { - return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionV2Error} + if _, isNoResult := decision.(*rpc.PermissionDecisionNoResult); isNoResult { + return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionDirectRpcError} + } + if _, isNoResult := decision.(rpc.PermissionDecisionNoResult); isNoResult { + return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionDirectRpcError} } - return &permissionResponseV2{Result: result}, nil + return &permissionResponseV2{Result: decision}, nil } diff --git a/go/internal/e2e/multi_client_e2e_test.go b/go/internal/e2e/multi_client_e2e_test.go index 9dd8a15bf..2a0b15c4f 100644 --- a/go/internal/e2e/multi_client_e2e_test.go +++ b/go/internal/e2e/multi_client_e2e_test.go @@ -10,6 +10,7 @@ import ( "time" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" "github.com/github/copilot-sdk/go/internal/e2e/testharness" ) @@ -139,11 +140,11 @@ func TestMultiClientE2E(t *testing.T) { // Client 1 creates a session and manually approves permission requests session1, err := client1.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { mu.Lock() client1PermissionRequests = append(client1PermissionRequests, request) mu.Unlock() - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -152,8 +153,8 @@ func TestMultiClientE2E(t *testing.T) { // Client 2 observes the permission request but leaves the decision to client 1. session2, err := client2.ResumeSession(t.Context(), session1.SessionID, &copilot.ResumeSessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionNoResult{}, nil }, }) if err != nil { @@ -239,8 +240,8 @@ func TestMultiClientE2E(t *testing.T) { // Client 1 creates a session and denies all permission requests session1, err := client1.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindRejected}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionReject{}, nil }, }) if err != nil { @@ -249,8 +250,8 @@ func TestMultiClientE2E(t *testing.T) { // Client 2 observes the permission request but leaves the decision to client 1. session2, err := client2.ResumeSession(t.Context(), session1.SessionID, &copilot.ResumeSessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionNoResult{}, nil }, }) if err != nil { diff --git a/go/internal/e2e/pending_work_resume_e2e_test.go b/go/internal/e2e/pending_work_resume_e2e_test.go index 41ca83021..552886413 100644 --- a/go/internal/e2e/pending_work_resume_e2e_test.go +++ b/go/internal/e2e/pending_work_resume_e2e_test.go @@ -40,14 +40,14 @@ func TestPendingWorkResumeE2E(t *testing.T) { }) permissionRequested := make(chan copilot.PermissionRequest, 1) - releasePermission := make(chan copilot.PermissionRequestResult, 1) + releasePermission := make(chan rpc.PermissionDecision, 1) suspendedClient := ctx.NewClient(func(opts *copilot.ClientOptions) { opts.Connection = copilot.UriConnection{URL: cliURL, ConnectionToken: sharedTcpToken} }) session1, err := suspendedClient.CreateSession(t.Context(), &copilot.SessionConfig{ Tools: []copilot.Tool{originalTool}, - OnPermissionRequest: func(req copilot.PermissionRequest, _ copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, _ copilot.PermissionInvocation) (rpc.PermissionDecision, error) { select { case permissionRequested <- req: default: @@ -114,8 +114,8 @@ func TestPendingWorkResumeE2E(t *testing.T) { session2, err := resumedClient.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ ContinuePendingWork: true, - OnPermissionRequest: func(_ copilot.PermissionRequest, _ copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil + OnPermissionRequest: func(_ copilot.PermissionRequest, _ copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionNoResult{}, nil }, Tools: []copilot.Tool{resumedTool}, }) @@ -154,7 +154,7 @@ func TestPendingWorkResumeE2E(t *testing.T) { // Allow original handler to unblock so cleanup proceeds. select { - case releasePermission <- copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindUserNotAvailable}: + case releasePermission <- &rpc.PermissionDecisionUserNotAvailable{}: default: } diff --git a/go/internal/e2e/permissions_e2e_test.go b/go/internal/e2e/permissions_e2e_test.go index bcc6fe278..5cbbfba1b 100644 --- a/go/internal/e2e/permissions_e2e_test.go +++ b/go/internal/e2e/permissions_e2e_test.go @@ -25,7 +25,7 @@ func TestPermissionsE2E(t *testing.T) { var permissionRequests []copilot.PermissionRequest var mu sync.Mutex - onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { mu.Lock() permissionRequests = append(permissionRequests, request) mu.Unlock() @@ -34,7 +34,7 @@ func TestPermissionsE2E(t *testing.T) { t.Error("Expected non-empty session ID in invocation") } - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ @@ -80,12 +80,12 @@ func TestPermissionsE2E(t *testing.T) { var permissionRequests []copilot.PermissionRequest var mu sync.Mutex - onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { mu.Lock() permissionRequests = append(permissionRequests, request) mu.Unlock() - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ @@ -119,8 +119,8 @@ func TestPermissionsE2E(t *testing.T) { t.Run("deny permission", func(t *testing.T) { ctx.ConfigureForTest(t) - onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindRejected}, nil + onPermissionRequest := func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionReject{}, nil } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ @@ -190,8 +190,8 @@ func TestPermissionsE2E(t *testing.T) { ctx.ConfigureForTest(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindUserNotAvailable}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionUserNotAvailable{}, nil }, }) if err != nil { @@ -240,8 +240,8 @@ func TestPermissionsE2E(t *testing.T) { } session2, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindUserNotAvailable}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionUserNotAvailable{}, nil }, }) if err != nil { @@ -309,9 +309,9 @@ func TestPermissionsE2E(t *testing.T) { var permissionRequestReceived atomicBool session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { permissionRequestReceived.Set(true) - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -348,9 +348,9 @@ func TestPermissionsE2E(t *testing.T) { var permissionRequestReceived atomicBool session2, err := client.ResumeSession(t.Context(), sessionID, &copilot.ResumeSessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { permissionRequestReceived.Set(true) - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -372,8 +372,8 @@ func TestPermissionsE2E(t *testing.T) { ctx.ConfigureForTest(t) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{}, fmt.Errorf("handler error") + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return nil, fmt.Errorf("handler error") }, }) if err != nil { @@ -409,11 +409,11 @@ func TestPermissionsE2E(t *testing.T) { var receivedToolCallID atomicBool session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { if shellReq, ok := req.(*copilot.PermissionRequestShell); ok && shellReq.ToolCallID != nil && *shellReq.ToolCallID != "" { receivedToolCallID.Set(true) } - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -450,10 +450,10 @@ func TestPermissionsE2E(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { shellReq, ok := req.(*copilot.PermissionRequestShell) if !ok { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil } toolCallID := "" if shellReq.ToolCallID != nil { @@ -470,7 +470,7 @@ func TestPermissionsE2E(t *testing.T) { } <-releaseHandler addLifecycle("permission-complete", toolCallID) - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -607,7 +607,7 @@ func TestPermissionsE2E(t *testing.T) { }), }, AvailableTools: []string{"first_permission_tool", "second_permission_tool"}, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { permissionRequestsMu.Lock() permissionRequestCount++ permissionRequests = append(permissionRequests, req) @@ -620,7 +620,7 @@ func TestPermissionsE2E(t *testing.T) { case <-bothStarted: case <-time.After(30 * time.Second): } - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -725,12 +725,12 @@ func TestPermissionsE2E(t *testing.T) { permissionCalled := make(chan struct{}, 1) session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { select { case permissionCalled <- struct{}{}: default: } - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil + return &rpc.PermissionDecisionNoResult{}, nil }, }) if err != nil { @@ -761,11 +761,11 @@ func TestPermissionsE2E(t *testing.T) { var handlerCallCountMu sync.Mutex session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { handlerCallCountMu.Lock() handlerCallCount++ handlerCallCountMu.Unlock() - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { diff --git a/go/internal/e2e/suspend_e2e_test.go b/go/internal/e2e/suspend_e2e_test.go index 8ce0c1fb1..98a15fb44 100644 --- a/go/internal/e2e/suspend_e2e_test.go +++ b/go/internal/e2e/suspend_e2e_test.go @@ -8,6 +8,7 @@ import ( "time" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" "github.com/github/copilot-sdk/go/internal/e2e/testharness" ) @@ -108,7 +109,7 @@ func TestSuspendE2E(t *testing.T) { } permissionRequested := make(chan copilot.PermissionRequest, 1) - releasePermission := make(chan copilot.PermissionRequestResult, 1) + releasePermission := make(chan rpc.PermissionDecision, 1) var toolInvoked atomic.Bool tool := copilot.DefineTool("suspend_cancel_permission_tool", "Transforms a value (should not run when suspend cancels permission)", @@ -119,7 +120,7 @@ func TestSuspendE2E(t *testing.T) { session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ Tools: []copilot.Tool{tool}, - OnPermissionRequest: func(request copilot.PermissionRequest, _ copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(request copilot.PermissionRequest, _ copilot.PermissionInvocation) (rpc.PermissionDecision, error) { select { case permissionRequested <- request: default: @@ -132,7 +133,7 @@ func TestSuspendE2E(t *testing.T) { } defer func() { select { - case releasePermission <- copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindUserNotAvailable}: + case releasePermission <- &rpc.PermissionDecisionUserNotAvailable{}: default: } }() diff --git a/go/internal/e2e/tools_e2e_test.go b/go/internal/e2e/tools_e2e_test.go index 014ced56f..6f9510e3d 100644 --- a/go/internal/e2e/tools_e2e_test.go +++ b/go/internal/e2e/tools_e2e_test.go @@ -9,6 +9,7 @@ import ( "testing" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" "github.com/github/copilot-sdk/go/internal/e2e/testharness" ) @@ -284,9 +285,9 @@ func TestToolsE2E(t *testing.T) { didRunPermissionRequest := false session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { didRunPermissionRequest = true - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindNoResult}, nil + return &rpc.PermissionDecisionNoResult{}, nil }, Tools: []copilot.Tool{ safeLookupTool, @@ -496,11 +497,11 @@ func TestToolsE2E(t *testing.T) { return strings.ToUpper(params.Input), nil }), }, - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { mu.Lock() permissionRequests = append(permissionRequests, request) mu.Unlock() - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -555,8 +556,8 @@ func TestToolsE2E(t *testing.T) { return strings.ToUpper(params.Input), nil }), }, - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindRejected}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionReject{}, nil }, }) if err != nil { diff --git a/go/permissions.go b/go/permissions.go index fb28851e3..f86a72683 100644 --- a/go/permissions.go +++ b/go/permissions.go @@ -1,11 +1,15 @@ package copilot +import ( + "github.com/github/copilot-sdk/go/rpc" +) + // PermissionHandler provides pre-built OnPermissionRequest implementations. var PermissionHandler = struct { // ApproveAll approves all permission requests. ApproveAll PermissionHandlerFunc }{ - ApproveAll: func(_ PermissionRequest, _ PermissionInvocation) (PermissionRequestResult, error) { - return PermissionRequestResult{Kind: PermissionRequestResultKindApproved}, nil + ApproveAll: func(_ PermissionRequest, _ PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, } diff --git a/go/rpc/permission_decision_no_result.go b/go/rpc/permission_decision_no_result.go new file mode 100644 index 000000000..3337120bc --- /dev/null +++ b/go/rpc/permission_decision_no_result.go @@ -0,0 +1,26 @@ +// Copyright (c) GitHub. All rights reserved. + +package rpc + +import "encoding/json" + +// PermissionDecisionNoResult is an SDK-only [PermissionDecision] value +// returned by a permission handler when it declines to respond to a +// request, allowing another connected client to answer instead. The SDK +// suppresses the response on the wire when it sees this variant. +type PermissionDecisionNoResult struct{} + +func (PermissionDecisionNoResult) permissionDecision() {} +func (PermissionDecisionNoResult) Kind() PermissionDecisionKind { + return PermissionDecisionKind("no-result") +} + +// MarshalJSON emits {"kind":"no-result"} for serialization symmetry with +// the other PermissionDecision variants. The SDK normally suppresses this +// value before it reaches the wire, but a stable representation is useful +// for tests and logging. +func (PermissionDecisionNoResult) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + Kind string `json:"kind"` + }{Kind: "no-result"}) +} diff --git a/go/session.go b/go/session.go index 8119a1bf5..da1324658 100644 --- a/go/session.go +++ b/go/session.go @@ -1143,7 +1143,7 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques SessionID: s.SessionID, } - result, err := handler(permissionRequest, invocation) + decision, err := handler(permissionRequest, invocation) if err != nil { s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.PermissionDecisionRequest{ RequestID: requestID, @@ -1151,29 +1151,19 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques }) return } - if result.Kind == "no-result" { + if _, ok := decision.(*rpc.PermissionDecisionNoResult); ok { + return + } + if _, ok := decision.(rpc.PermissionDecisionNoResult); ok { return } s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.PermissionDecisionRequest{ RequestID: requestID, - Result: rpcPermissionDecisionFromKind(rpc.PermissionDecisionKind(result.Kind)), + Result: decision, }) } -func rpcPermissionDecisionFromKind(kind rpc.PermissionDecisionKind) rpc.PermissionDecision { - switch kind { - case rpc.PermissionDecisionKindApproveOnce: - return &rpc.PermissionDecisionApproveOnce{} - case rpc.PermissionDecisionKindReject: - return &rpc.PermissionDecisionReject{} - case rpc.PermissionDecisionKindUserNotAvailable: - return &rpc.PermissionDecisionUserNotAvailable{} - default: - return &rpc.RawPermissionDecisionData{Discriminator: kind} - } -} - // GetEvents retrieves all events from this session's history. // // This returns the complete conversation history including user messages, diff --git a/go/session_test.go b/go/session_test.go index 0b7de5ac9..16ac64273 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -8,8 +8,6 @@ import ( "sync/atomic" "testing" "time" - - "github.com/github/copilot-sdk/go/rpc" ) // newTestSession creates a session with an event channel and starts the consumer goroutine. @@ -28,24 +26,6 @@ func newTestEvent() SessionEvent { return SessionEvent{Data: &SessionIdleData{}} } -func TestRPCPermissionDecisionFromKindPreservesUnknownKind(t *testing.T) { - kind := rpc.PermissionDecisionKind("future-decision") - decision := rpcPermissionDecisionFromKind(kind) - - data, err := json.Marshal(decision) - if err != nil { - t.Fatalf("marshal permission decision: %v", err) - } - - var serialized map[string]any - if err := json.Unmarshal(data, &serialized); err != nil { - t.Fatalf("unmarshal serialized permission decision: %v", err) - } - if serialized["kind"] != string(kind) { - t.Fatalf("expected kind %q to round-trip, got %v in %s", kind, serialized["kind"], data) - } -} - func TestSession_On(t *testing.T) { t.Run("multiple handlers all receive events", func(t *testing.T) { session, cleanup := newTestSession() diff --git a/go/types.go b/go/types.go index 2760e9b2b..40689f42d 100644 --- a/go/types.go +++ b/go/types.go @@ -268,42 +268,17 @@ type SystemMessageConfig struct { Sections map[string]SectionOverride `json:"sections,omitempty"` } -// PermissionRequestResultKind represents the kind of a permission request result. -type PermissionRequestResultKind string - -const ( - // PermissionRequestResultKindApproved indicates the permission was approved for this one instance. - PermissionRequestResultKindApproved PermissionRequestResultKind = "approve-once" - - // PermissionRequestResultKindRejected indicates the permission was denied interactively by the user. - PermissionRequestResultKindRejected PermissionRequestResultKind = "reject" - - // PermissionRequestResultKindUserNotAvailable indicates the permission was denied because - // user confirmation was unavailable. - PermissionRequestResultKindUserNotAvailable PermissionRequestResultKind = "user-not-available" - - // PermissionRequestResultKindNoResult indicates no permission decision was made. - PermissionRequestResultKindNoResult PermissionRequestResultKind = "no-result" - - // Deprecated: Use PermissionRequestResultKindRejected instead. - PermissionRequestResultKindDeniedInteractivelyByUser = PermissionRequestResultKindRejected - - // Deprecated: Use PermissionRequestResultKindUserNotAvailable instead. - PermissionRequestResultKindDeniedCouldNotRequestFromUser = PermissionRequestResultKindUserNotAvailable - - // Deprecated: Use PermissionRequestResultKindUserNotAvailable instead. - PermissionRequestResultKindDeniedByRules = PermissionRequestResultKindUserNotAvailable -) - -// PermissionRequestResult represents the result of a permission request -type PermissionRequestResult struct { - Kind PermissionRequestResultKind `json:"kind"` - Rules []any `json:"rules,omitempty"` -} - -// PermissionHandlerFunc executes a permission request -// The handler should return a PermissionRequestResult. Returning an error denies the permission. -type PermissionHandlerFunc func(request PermissionRequest, invocation PermissionInvocation) (PermissionRequestResult, error) +// PermissionHandlerFunc executes a permission request. +// The handler should return a [rpc.PermissionDecision]. Returning an error +// causes the SDK to respond with [rpc.PermissionDecisionUserNotAvailable]. +// +// Use the variant types directly: +// +// &rpc.PermissionDecisionApproveOnce{} +// &rpc.PermissionDecisionReject{Feedback: &feedback} +// &rpc.PermissionDecisionUserNotAvailable{} +// &rpc.PermissionDecisionNoResult{} // decline to respond; another client may answer +type PermissionHandlerFunc func(request PermissionRequest, invocation PermissionInvocation) (rpc.PermissionDecision, error) // PermissionInvocation provides context about a permission request type PermissionInvocation struct { diff --git a/go/types_test.go b/go/types_test.go index 1d201d2b8..6d83c8ec0 100644 --- a/go/types_test.go +++ b/go/types_test.go @@ -5,96 +5,6 @@ import ( "testing" ) -func TestPermissionRequestResultKind_Constants(t *testing.T) { - tests := []struct { - name string - kind PermissionRequestResultKind - expected string - }{ - {"Approved", PermissionRequestResultKindApproved, "approve-once"}, - {"Rejected", PermissionRequestResultKindRejected, "reject"}, - {"UserNotAvailable", PermissionRequestResultKindUserNotAvailable, "user-not-available"}, - {"NoResult", PermissionRequestResultKindNoResult, "no-result"}, - // Deprecated aliases - {"DeprecatedDeniedInteractivelyByUser", PermissionRequestResultKindDeniedInteractivelyByUser, "reject"}, - {"DeprecatedDeniedCouldNotRequestFromUser", PermissionRequestResultKindDeniedCouldNotRequestFromUser, "user-not-available"}, - {"DeprecatedDeniedByRules", PermissionRequestResultKindDeniedByRules, "user-not-available"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if string(tt.kind) != tt.expected { - t.Errorf("expected %q, got %q", tt.expected, string(tt.kind)) - } - }) - } -} - -func TestPermissionRequestResultKind_CustomValue(t *testing.T) { - custom := PermissionRequestResultKind("custom-kind") - if string(custom) != "custom-kind" { - t.Errorf("expected %q, got %q", "custom-kind", string(custom)) - } -} - -func TestPermissionRequestResult_JSONRoundTrip(t *testing.T) { - tests := []struct { - name string - kind PermissionRequestResultKind - }{ - {"Approved", PermissionRequestResultKindApproved}, - {"DeniedByRules", PermissionRequestResultKindDeniedByRules}, - {"DeniedCouldNotRequestFromUser", PermissionRequestResultKindDeniedCouldNotRequestFromUser}, - {"DeniedInteractivelyByUser", PermissionRequestResultKindDeniedInteractivelyByUser}, - {"NoResult", PermissionRequestResultKind("no-result")}, - {"Custom", PermissionRequestResultKind("custom")}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - original := PermissionRequestResult{Kind: tt.kind} - data, err := json.Marshal(original) - if err != nil { - t.Fatalf("failed to marshal: %v", err) - } - - var decoded PermissionRequestResult - if err := json.Unmarshal(data, &decoded); err != nil { - t.Fatalf("failed to unmarshal: %v", err) - } - - if decoded.Kind != tt.kind { - t.Errorf("expected kind %q, got %q", tt.kind, decoded.Kind) - } - }) - } -} - -func TestPermissionRequestResult_JSONDeserialize(t *testing.T) { - jsonStr := `{"kind":"reject"}` - var result PermissionRequestResult - if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { - t.Fatalf("failed to unmarshal: %v", err) - } - - if result.Kind != PermissionRequestResultKindRejected { - t.Errorf("expected %q, got %q", PermissionRequestResultKindRejected, result.Kind) - } -} - -func TestPermissionRequestResult_JSONSerialize(t *testing.T) { - result := PermissionRequestResult{Kind: PermissionRequestResultKindApproved} - data, err := json.Marshal(result) - if err != nil { - t.Fatalf("failed to marshal: %v", err) - } - - expected := `{"kind":"approve-once"}` - if string(data) != expected { - t.Errorf("expected %s, got %s", expected, string(data)) - } -} - func TestProviderConfig_JSONIncludesHeaders(t *testing.T) { config := ProviderConfig{ BaseURL: "https://example.com/provider", diff --git a/test/scenarios/callbacks/hooks/csharp/Program.cs b/test/scenarios/callbacks/hooks/csharp/Program.cs index 1325565e8..b20addf39 100644 --- a/test/scenarios/callbacks/hooks/csharp/Program.cs +++ b/test/scenarios/callbacks/hooks/csharp/Program.cs @@ -1,4 +1,5 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; var hookLog = new List(); @@ -12,7 +13,7 @@ { Model = "claude-haiku-4.5", OnPermissionRequest = (request, invocation) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), Hooks = new SessionHooks { OnSessionStart = (input, invocation) => diff --git a/test/scenarios/callbacks/hooks/go/main.go b/test/scenarios/callbacks/hooks/go/main.go index 01411101e..d080a6ae1 100644 --- a/test/scenarios/callbacks/hooks/go/main.go +++ b/test/scenarios/callbacks/hooks/go/main.go @@ -7,6 +7,7 @@ import ( "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -31,8 +32,8 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, Hooks: &copilot.SessionHooks{ OnSessionStart: func(input copilot.SessionStartHookInput, inv copilot.HookInvocation) (*copilot.SessionStartHookOutput, error) { diff --git a/test/scenarios/callbacks/permissions/csharp/Program.cs b/test/scenarios/callbacks/permissions/csharp/Program.cs index 1e309e71a..7818580fb 100644 --- a/test/scenarios/callbacks/permissions/csharp/Program.cs +++ b/test/scenarios/callbacks/permissions/csharp/Program.cs @@ -1,4 +1,5 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; var permissionLog = new List(); @@ -23,7 +24,7 @@ _ => request.Kind, }; permissionLog.Add($"approved:{toolName}"); - return Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }); + return Task.FromResult(PermissionDecision.ApproveOnce()); }, Hooks = new SessionHooks { diff --git a/test/scenarios/callbacks/permissions/go/main.go b/test/scenarios/callbacks/permissions/go/main.go index 489db10e1..428ab0a70 100644 --- a/test/scenarios/callbacks/permissions/go/main.go +++ b/test/scenarios/callbacks/permissions/go/main.go @@ -7,6 +7,7 @@ import ( "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -25,7 +26,7 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { permissionLogMu.Lock() permissionName := string(req.Kind()) switch request := req.(type) { @@ -38,7 +39,7 @@ func main() { } permissionLog = append(permissionLog, fmt.Sprintf("approved:%s", permissionName)) permissionLogMu.Unlock() - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + return &rpc.PermissionDecisionApproveOnce{}, nil }, Hooks: &copilot.SessionHooks{ OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { diff --git a/test/scenarios/callbacks/user-input/csharp/Program.cs b/test/scenarios/callbacks/user-input/csharp/Program.cs index a89d2cd98..e6988c6cb 100644 --- a/test/scenarios/callbacks/user-input/csharp/Program.cs +++ b/test/scenarios/callbacks/user-input/csharp/Program.cs @@ -1,4 +1,5 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; var inputLog = new List(); @@ -12,7 +13,7 @@ { Model = "claude-haiku-4.5", OnPermissionRequest = (request, invocation) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), OnUserInputRequest = (request, invocation) => { inputLog.Add($"question: {request.Question}"); diff --git a/test/scenarios/callbacks/user-input/go/main.go b/test/scenarios/callbacks/user-input/go/main.go index f3c58e253..673667058 100644 --- a/test/scenarios/callbacks/user-input/go/main.go +++ b/test/scenarios/callbacks/user-input/go/main.go @@ -7,6 +7,7 @@ import ( "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) var ( @@ -25,8 +26,8 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, OnUserInputRequest: func(req copilot.UserInputRequest, inv copilot.UserInputInvocation) (copilot.UserInputResponse, error) { inputLogMu.Lock() diff --git a/test/scenarios/tools/skills/csharp/Program.cs b/test/scenarios/tools/skills/csharp/Program.cs index 5436619aa..5e3f3c859 100644 --- a/test/scenarios/tools/skills/csharp/Program.cs +++ b/test/scenarios/tools/skills/csharp/Program.cs @@ -1,4 +1,5 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; using var client = new CopilotClient(); @@ -13,7 +14,7 @@ Model = "claude-haiku-4.5", SkillDirectories = [skillsDir], OnPermissionRequest = (request, invocation) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), Hooks = new SessionHooks { OnPreToolUse = (input, invocation) => diff --git a/test/scenarios/tools/skills/go/main.go b/test/scenarios/tools/skills/go/main.go index 57bb9ce14..21d9604f7 100644 --- a/test/scenarios/tools/skills/go/main.go +++ b/test/scenarios/tools/skills/go/main.go @@ -8,6 +8,7 @@ import ( "runtime" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -25,8 +26,8 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "claude-haiku-4.5", SkillDirectories: []string{skillsDir}, - OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(request copilot.PermissionRequest, invocation copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, Hooks: &copilot.SessionHooks{ OnPreToolUse: func(input copilot.PreToolUseHookInput, invocation copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { diff --git a/test/scenarios/tools/virtual-filesystem/csharp/Program.cs b/test/scenarios/tools/virtual-filesystem/csharp/Program.cs index 1081be345..64704ff3f 100644 --- a/test/scenarios/tools/virtual-filesystem/csharp/Program.cs +++ b/test/scenarios/tools/virtual-filesystem/csharp/Program.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using GitHub.Copilot; +using GitHub.Copilot.Rpc; using Microsoft.Extensions.AI; // In-memory virtual filesystem @@ -45,7 +46,7 @@ "List all files in the virtual filesystem"), ], OnPermissionRequest = (request, invocation) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), Hooks = new SessionHooks { OnPreToolUse = (input, invocation) => diff --git a/test/scenarios/tools/virtual-filesystem/go/main.go b/test/scenarios/tools/virtual-filesystem/go/main.go index 26737184c..84dccf7f4 100644 --- a/test/scenarios/tools/virtual-filesystem/go/main.go +++ b/test/scenarios/tools/virtual-filesystem/go/main.go @@ -8,6 +8,7 @@ import ( "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) // In-memory virtual filesystem @@ -85,8 +86,8 @@ func main() { // Remove all built-in tools — only our custom virtual FS tools are available AvailableTools: []string{}, Tools: []copilot.Tool{createFile, readFile, listFiles}, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, Hooks: &copilot.SessionHooks{ OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) { From eb591c9d5cd8964e0ab7c791164d6d10ffc1736b Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 16:17:50 +0100 Subject: [PATCH 06/14] Rust: replace PermissionResult flat enum with PermissionDecision wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same shape as the .NET and Go refactors in the previous commit. The hand-written ``PermissionResult`` enum lost almost everything the generated ``PermissionDecision`` discriminated union exposes — feedback strings on rejection, per-kind approval lists on ``ApproveForSession``/``ApproveForLocation``, domains on ``ApprovePermanently``, the rich denial reasons (``DeniedByRules``, ``DeniedByContentExclusionPolicy``, …). It also carried two SDK-only escape hatches (``Deferred``, ``Custom(serde_json::Value)``) that other SDKs don't expose and that consumers never actually needed. This commit: - Replaces ``PermissionResult`` with a thin two-variant wrapper: ``PermissionResult::Decision(PermissionDecision)`` and ``PermissionResult::NoResult``. The ``Decision`` variant carries any wire-level decision; ``NoResult`` tells the SDK to suppress the response so another connected client can answer. - Adds discoverable factory methods on ``PermissionResult``: ``approve_once()``, ``reject(feedback)``, ``user_not_available()``, ``no_result()``. For richer decisions (per-session, per-location, permanent) consumers construct the ``PermissionDecision`` variant directly and wrap it in ``PermissionResult::Decision`` (the ``From`` impl also covers this). - Drops ``PermissionResult::Approved`` / ``Denied`` / ``UserNotAvailable`` / ``Deferred`` / ``Custom`` and the ``permission_request_response`` / ``pending_permission_result_kind`` helpers — the new wrapper serializes its inner ``PermissionDecision`` directly via serde. Updates ``ApproveAllHandler`` / ``DenyAllHandler`` / ``PolicyHandler``, all session dispatch logic, internal unit tests, the E2E test suite, scenarios, and the README to construct decisions via the factories. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/README.md | 4 +- rust/src/handler.rs | 84 +++++++--- rust/src/permission.rs | 18 +- rust/src/session.rs | 157 ++++-------------- rust/src/types.rs | 66 ++++++-- rust/tests/e2e/ask_user.rs | 2 +- rust/tests/e2e/elicitation.rs | 2 +- rust/tests/e2e/multi_client.rs | 6 +- .../e2e/multi_client_commands_elicitation.rs | 2 +- rust/tests/e2e/permissions.rs | 23 ++- rust/tests/e2e/tools.rs | 6 +- rust/tests/session_test.rs | 2 +- .../callbacks/permissions/rust/src/main.rs | 2 +- .../callbacks/user-input/rust/src/main.rs | 2 +- 14 files changed, 182 insertions(+), 194 deletions(-) diff --git a/rust/README.md b/rust/README.md index 2b3a5423c..b30dc1049 100644 --- a/rust/README.md +++ b/rust/README.md @@ -207,9 +207,9 @@ impl PermissionHandler for MyPermissions { data: PermissionRequestData, ) -> PermissionResult { if data.extra.get("tool").and_then(|v| v.as_str()) == Some("view") { - PermissionResult::Approved + PermissionResult::approve_once() } else { - PermissionResult::Denied + PermissionResult::reject(None) } } } diff --git a/rust/src/handler.rs b/rust/src/handler.rs index 042565564..dadd1706f 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -18,34 +18,64 @@ use async_trait::async_trait; use serde::{Deserialize, Serialize}; +use crate::generated::api_types::{ + PermissionDecision, PermissionDecisionApproveOnce, PermissionDecisionReject, + PermissionDecisionUserNotAvailable, +}; use crate::types::{ ElicitationRequest, ElicitationResult, ExitPlanModeData, PermissionRequestData, RequestId, SessionId, }; -/// Result of a permission request. +/// Decision returned by a [`PermissionHandler`]. +/// +/// Either a concrete wire-level [`PermissionDecision`] (approve, reject, +/// approve-for-session, approve-permanently, user-not-available, …) or +/// [`PermissionResult::NoResult`], which tells the SDK to suppress its +/// response so another connected client can answer instead. #[derive(Debug, Clone)] -#[non_exhaustive] pub enum PermissionResult { - /// Permission granted. - Approved, - /// Permission denied. - Denied, - /// Defer the response. The handler will resolve this request itself - /// later -- typically after a UI prompt -- by calling - /// `session.permissions.handlePendingPermissionRequest` directly. The - /// SDK skips its own response on this path. - Deferred, - /// Provide the full response payload directly. The SDK forwards the - /// value as-is on the wire. - Custom(serde_json::Value), - /// Decline to handle this broadcast. The SDK does not send a response, - /// which lets another connected client respond instead. + /// Send a permission decision on the wire. + Decision(PermissionDecision), + /// Decline to respond to this request, allowing another connected + /// client to answer instead. The SDK suppresses the response. NoResult, - /// No user is available to answer the prompt. On the notification - /// path, the SDK will not send a pending response. On the direct - /// RPC path, the SDK responds with `{ "kind": "user-not-available" }`. - UserNotAvailable, +} + +impl PermissionResult { + /// Approve this single request. + pub fn approve_once() -> Self { + Self::Decision(PermissionDecision::ApproveOnce( + PermissionDecisionApproveOnce::default(), + )) + } + + /// Reject the request, optionally forwarding feedback to the LLM. + pub fn reject(feedback: impl Into>) -> Self { + Self::Decision(PermissionDecision::Reject(PermissionDecisionReject { + feedback: feedback.into(), + ..Default::default() + })) + } + + /// Deny because no user is available to confirm. + pub fn user_not_available() -> Self { + Self::Decision(PermissionDecision::UserNotAvailable( + PermissionDecisionUserNotAvailable::default(), + )) + } + + /// Decline to respond, allowing another connected client to answer + /// instead. + pub fn no_result() -> Self { + Self::NoResult + } +} + +impl From for PermissionResult { + fn from(value: PermissionDecision) -> Self { + Self::Decision(value) + } } /// Response to a user input request. @@ -183,7 +213,7 @@ impl PermissionHandler for ApproveAllHandler { _request_id: RequestId, _data: PermissionRequestData, ) -> PermissionResult { - PermissionResult::Approved + PermissionResult::approve_once() } } @@ -199,7 +229,7 @@ impl PermissionHandler for DenyAllHandler { _request_id: RequestId, _data: PermissionRequestData, ) -> PermissionResult { - PermissionResult::Denied + PermissionResult::reject(None) } } @@ -216,7 +246,10 @@ mod tests { PermissionRequestData::default(), ) .await; - assert!(matches!(result, PermissionResult::Approved)); + assert!(matches!( + result, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); } #[tokio::test] @@ -228,6 +261,9 @@ mod tests { PermissionRequestData::default(), ) .await; - assert!(matches!(result, PermissionResult::Denied)); + assert!(matches!( + result, + PermissionResult::Decision(PermissionDecision::Reject(_)) + )); } } diff --git a/rust/src/permission.rs b/rust/src/permission.rs index 22cf9bda9..2ddd773a3 100644 --- a/rust/src/permission.rs +++ b/rust/src/permission.rs @@ -116,9 +116,9 @@ impl PermissionHandler for PolicyHandler { Policy::Predicate(f) => f(&data), }; if approved { - PermissionResult::Approved + PermissionResult::approve_once() } else { - PermissionResult::Denied + PermissionResult::reject(None) } } } @@ -140,7 +140,7 @@ mod tests { assert!(matches!( h.handle(SessionId::from("s"), RequestId::new("1"), data()) .await, - PermissionResult::Approved + PermissionResult::Decision(crate::types::PermissionDecision::ApproveOnce(_)) )); } @@ -150,7 +150,7 @@ mod tests { assert!(matches!( h.handle(SessionId::from("s"), RequestId::new("1"), data()) .await, - PermissionResult::Denied + PermissionResult::Decision(crate::types::PermissionDecision::Reject(_)) )); } @@ -160,7 +160,7 @@ mod tests { assert!(matches!( h.handle(SessionId::from("s"), RequestId::new("1"), data()) .await, - PermissionResult::Denied + PermissionResult::Decision(crate::types::PermissionDecision::Reject(_)) )); } @@ -175,7 +175,7 @@ mod tests { _: RequestId, _: PermissionRequestData, ) -> PermissionResult { - PermissionResult::Approved + PermissionResult::approve_once() } } let resolved = @@ -185,7 +185,7 @@ mod tests { resolved .handle(SessionId::from("s"), RequestId::new("1"), data()) .await, - PermissionResult::Denied + PermissionResult::Decision(crate::types::PermissionDecision::Reject(_)) )); } @@ -200,7 +200,7 @@ mod tests { _: RequestId, _: PermissionRequestData, ) -> PermissionResult { - PermissionResult::Approved + PermissionResult::approve_once() } } let resolved = resolve_handler(Some(Arc::new(H)), None).unwrap(); @@ -208,7 +208,7 @@ mod tests { resolved .handle(SessionId::from("s"), RequestId::new("1"), data()) .await, - PermissionResult::Approved + PermissionResult::Decision(crate::types::PermissionDecision::ApproveOnce(_)) )); } diff --git a/rust/src/session.rs b/rust/src/session.rs index 842d5d732..138d1f1a7 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -10,10 +10,7 @@ use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tracing::{Instrument, warn}; -use crate::generated::api_types::{ - LogRequest, ModelSwitchToRequest, PermissionDecision, PermissionDecisionApproveOnce, - PermissionDecisionApproveOnceKind, PermissionDecisionReject, PermissionDecisionRejectKind, -}; +use crate::generated::api_types::{LogRequest, ModelSwitchToRequest}; use crate::generated::session_events::{ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, SessionErrorData, SessionEventType, @@ -1203,63 +1200,30 @@ fn extract_request_id(data: &Value) -> Option { .map(RequestId::new) } -fn pending_permission_result_kind(result: &PermissionResult) -> &'static str { - match result { - PermissionResult::Approved => "approve-once", - PermissionResult::Denied => "reject", - // Fallback to "user-not-available" for UserNotAvailable, Deferred (when - // forced through this path), Custom (handled separately upstream), and - // NoResult that gets here defensively. - _ => "user-not-available", - } -} - -fn permission_request_response(result: &PermissionResult) -> PermissionDecision { - match result { - PermissionResult::Approved => { - PermissionDecision::ApproveOnce(PermissionDecisionApproveOnce { - kind: PermissionDecisionApproveOnceKind::ApproveOnce, - }) - } - _ => PermissionDecision::Reject(PermissionDecisionReject { - kind: PermissionDecisionRejectKind::Reject, - feedback: None, - }), - } -} - -/// Map a permission result into the `result` payload for the notification -/// path (`session.permissions.handlePendingPermissionRequest`). +/// Map a [`PermissionResult`] to the `result` payload for the +/// broadcast-event response path (`session.permissions.handlePendingPermissionRequest`). /// -/// Returns `None` when the SDK must not respond. +/// Returns `None` when the SDK must not send a response. fn notification_permission_payload(result: &PermissionResult) -> Option { match result { - PermissionResult::Deferred | PermissionResult::NoResult => None, - PermissionResult::Custom(value) => Some(value.clone()), - _ => Some(serde_json::json!({ - "kind": pending_permission_result_kind(result), - })), + PermissionResult::NoResult => None, + PermissionResult::Decision(decision) => Some( + serde_json::to_value(decision).expect("serializing permission decision should succeed"), + ), } } -/// Map a permission result into the JSON-RPC `result` payload for the +/// Map a [`PermissionResult`] to the JSON-RPC `result` payload for the /// direct-RPC path (`permission.request`). /// -/// Always returns a value. [`PermissionResult::Deferred`] is treated as -/// [`PermissionResult::Approved`] here because the JSON-RPC contract -/// requires a reply — see the variant's doc comment. +/// Always returns a value. `NoResult` is mapped to `user-not-available` +/// because the JSON-RPC contract requires a reply. fn direct_permission_payload(result: &PermissionResult) -> Value { match result { - PermissionResult::Custom(value) => value.clone(), - PermissionResult::Deferred => { - serde_json::to_value(permission_request_response(&PermissionResult::Approved)) - .expect("serializing direct permission response should succeed") + PermissionResult::Decision(decision) => { + serde_json::to_value(decision).expect("serializing permission decision should succeed") } - PermissionResult::NoResult | PermissionResult::UserNotAvailable => serde_json::json!({ - "kind": pending_permission_result_kind(result), - }), - _ => serde_json::to_value(permission_request_response(result)) - .expect("serializing direct permission response should succeed"), + PermissionResult::NoResult => serde_json::json!({ "kind": "user-not-available" }), } } @@ -2139,107 +2103,50 @@ fn inject_transform_sections_resume( mod tests { use serde_json::json; - use super::{ - direct_permission_payload, notification_permission_payload, pending_permission_result_kind, - permission_request_response, - }; + use super::{direct_permission_payload, notification_permission_payload}; use crate::handler::PermissionResult; #[test] - fn pending_permission_requests_use_decision_kinds() { - assert_eq!( - pending_permission_result_kind(&PermissionResult::Approved), - "approve-once" - ); - assert_eq!( - pending_permission_result_kind(&PermissionResult::Denied), - "reject" - ); - assert_eq!( - pending_permission_result_kind(&PermissionResult::UserNotAvailable), - "user-not-available" - ); - } - - #[test] - fn direct_permission_requests_use_decision_response_kinds() { - assert_eq!( - serde_json::to_value(permission_request_response(&PermissionResult::Approved)) - .expect("serializing approved permission response should succeed"), - json!({ "kind": "approve-once" }) - ); - assert_eq!( - serde_json::to_value(permission_request_response(&PermissionResult::Denied)) - .expect("serializing denied permission response should succeed"), - json!({ "kind": "reject" }) - ); - assert_eq!( - serde_json::to_value(permission_request_response( - &PermissionResult::UserNotAvailable - )) - .expect("serializing fallback permission response should succeed"), - json!({ "kind": "reject" }) - ); + fn notification_payload_suppresses_no_result() { + assert!(notification_permission_payload(&PermissionResult::NoResult).is_none()); } #[test] - fn notification_payload_handles_non_responses_and_custom() { - // Deferred/NoResult -> no payload, SDK must not respond. - assert!(notification_permission_payload(&PermissionResult::Deferred).is_none()); - assert!(notification_permission_payload(&PermissionResult::NoResult).is_none()); - - // Custom → handler-supplied value passed through verbatim. - let custom = json!({ - "kind": "approve-and-remember", - "allowlist": ["ls", "grep"], - }); - assert_eq!( - notification_permission_payload(&PermissionResult::Custom(custom.clone())), - Some(custom) - ); - - // Approved/Denied → existing kind-only shape. + fn notification_payload_serializes_decisions() { assert_eq!( - notification_permission_payload(&PermissionResult::Approved), + notification_permission_payload(&PermissionResult::approve_once()), Some(json!({ "kind": "approve-once" })) ); assert_eq!( - notification_permission_payload(&PermissionResult::Denied), + notification_permission_payload(&PermissionResult::reject(None)), Some(json!({ "kind": "reject" })) ); - } - - #[test] - fn direct_payload_handles_deferred_and_custom() { - // Custom → handler-supplied value passed through verbatim. - let custom = json!({ - "kind": "approve-and-remember", - "allowlist": ["ls", "grep"], - }); assert_eq!( - direct_permission_payload(&PermissionResult::Custom(custom.clone())), - custom + notification_permission_payload(&PermissionResult::reject(Some("bad".to_string()))), + Some(json!({ "kind": "reject", "feedback": "bad" })) ); - - // Deferred → falls back to Approved because the direct RPC must reply. assert_eq!( - direct_permission_payload(&PermissionResult::Deferred), - json!({ "kind": "approve-once" }) + notification_permission_payload(&PermissionResult::user_not_available()), + Some(json!({ "kind": "user-not-available" })) ); + } - // NoResult -> direct RPC cannot be left pending, so report no user. + #[test] + fn direct_payload_maps_no_result_to_user_not_available() { assert_eq!( direct_permission_payload(&PermissionResult::NoResult), json!({ "kind": "user-not-available" }) ); + } - // Approved/Denied → existing kind-only shape. + #[test] + fn direct_payload_serializes_decisions() { assert_eq!( - direct_permission_payload(&PermissionResult::Approved), + direct_permission_payload(&PermissionResult::approve_once()), json!({ "kind": "approve-once" }) ); assert_eq!( - direct_permission_payload(&PermissionResult::Denied), + direct_permission_payload(&PermissionResult::reject(None)), json!({ "kind": "reject" }) ); } diff --git a/rust/src/types.rs b/rust/src/types.rs index 2830257fc..3453b1879 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -3293,7 +3293,8 @@ impl InputFormat { /// `pub use types::*` surfaces them alongside hand-written SDK types. pub use crate::generated::api_types::{ Model, ModelBilling, ModelCapabilities, ModelCapabilitiesLimits, ModelCapabilitiesLimitsVision, - ModelCapabilitiesSupports, ModelList, ModelPolicy, + ModelCapabilitiesSupports, ModelList, ModelPolicy, PermissionDecision, + PermissionDecisionApproveOnce, PermissionDecisionReject, PermissionDecisionUserNotAvailable, }; /// Permission categories the CLI may request approval for. @@ -4060,7 +4061,8 @@ mod permission_builder_tests { use crate::handler::{ApproveAllHandler, PermissionHandler, PermissionResult}; use crate::permission; use crate::types::{ - PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, SessionId, + PermissionDecision, PermissionRequestData, RequestId, ResumeSessionConfig, SessionConfig, + SessionId, }; fn data() -> PermissionRequestData { @@ -4092,14 +4094,20 @@ mod permission_builder_tests { .with_permission_handler(Arc::new(ApproveAllHandler)) .approve_all_permissions(); let h = resolve_create(cfg).expect("policy + handler yields handler"); - assert!(matches!(dispatch(&h).await, PermissionResult::Approved)); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); } #[tokio::test] async fn approve_all_standalone_produces_handler() { let cfg = SessionConfig::default().approve_all_permissions(); let h = resolve_create(cfg).expect("policy alone yields handler"); - assert!(matches!(dispatch(&h).await, PermissionResult::Approved)); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); } /// Phase I: order between with_permission_handler and the policy @@ -4114,8 +4122,14 @@ mod permission_builder_tests { .with_permission_handler(Arc::new(ApproveAllHandler)); let ha = resolve_create(a).unwrap(); let hb = resolve_create(b).unwrap(); - assert!(matches!(dispatch(&ha).await, PermissionResult::Approved)); - assert!(matches!(dispatch(&hb).await, PermissionResult::Approved)); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); } #[tokio::test] @@ -4128,8 +4142,14 @@ mod permission_builder_tests { .with_permission_handler(Arc::new(ApproveAllHandler)); let ha = resolve_create(a).unwrap(); let hb = resolve_create(b).unwrap(); - assert!(matches!(dispatch(&ha).await, PermissionResult::Denied)); - assert!(matches!(dispatch(&hb).await, PermissionResult::Denied)); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision(PermissionDecision::Reject(_)) + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision(PermissionDecision::Reject(_)) + )); } #[tokio::test] @@ -4138,7 +4158,10 @@ mod permission_builder_tests { d.extra.get("tool").and_then(|v| v.as_str()) != Some("shell") }); let h = resolve_create(cfg).unwrap(); - assert!(matches!(dispatch(&h).await, PermissionResult::Denied)); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision(PermissionDecision::Reject(_)) + )); } #[tokio::test] @@ -4154,8 +4177,14 @@ mod permission_builder_tests { .with_permission_handler(Arc::new(ApproveAllHandler)); let ha = resolve_create(a).unwrap(); let hb = resolve_create(b).unwrap(); - assert!(matches!(dispatch(&ha).await, PermissionResult::Denied)); - assert!(matches!(dispatch(&hb).await, PermissionResult::Denied)); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision(PermissionDecision::Reject(_)) + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision(PermissionDecision::Reject(_)) + )); } #[tokio::test] @@ -4164,7 +4193,10 @@ mod permission_builder_tests { .with_permission_handler(Arc::new(ApproveAllHandler)) .approve_all_permissions(); let h = resolve_resume(cfg).unwrap(); - assert!(matches!(dispatch(&h).await, PermissionResult::Approved)); + assert!(matches!( + dispatch(&h).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); } #[tokio::test] @@ -4177,7 +4209,13 @@ mod permission_builder_tests { .with_permission_handler(Arc::new(ApproveAllHandler)); let ha = resolve_resume(a).unwrap(); let hb = resolve_resume(b).unwrap(); - assert!(matches!(dispatch(&ha).await, PermissionResult::Approved)); - assert!(matches!(dispatch(&hb).await, PermissionResult::Approved)); + assert!(matches!( + dispatch(&ha).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); + assert!(matches!( + dispatch(&hb).await, + PermissionResult::Decision(PermissionDecision::ApproveOnce(_)) + )); } } diff --git a/rust/tests/e2e/ask_user.rs b/rust/tests/e2e/ask_user.rs index d9548235d..282af7d30 100644 --- a/rust/tests/e2e/ask_user.rs +++ b/rust/tests/e2e/ask_user.rs @@ -201,6 +201,6 @@ impl PermissionHandler for RecordingUserInputHandler { _request_id: RequestId, _data: github_copilot_sdk::PermissionRequestData, ) -> PermissionResult { - PermissionResult::Approved + PermissionResult::approve_once() } } diff --git a/rust/tests/e2e/elicitation.rs b/rust/tests/e2e/elicitation.rs index 91961e60f..5d38ee132 100644 --- a/rust/tests/e2e/elicitation.rs +++ b/rust/tests/e2e/elicitation.rs @@ -545,7 +545,7 @@ impl PermissionHandler for QueuedElicitationHandler { _request_id: RequestId, _data: github_copilot_sdk::PermissionRequestData, ) -> PermissionResult { - PermissionResult::Approved + PermissionResult::approve_once() } } diff --git a/rust/tests/e2e/multi_client.rs b/rust/tests/e2e/multi_client.rs index 836121644..7566fb063 100644 --- a/rust/tests/e2e/multi_client.rs +++ b/rust/tests/e2e/multi_client.rs @@ -119,7 +119,7 @@ async fn one_client_approves_permission_and_both_see_the_result() { SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) .with_permission_handler(permission_handler_with_counter( - PermissionResult::Approved, + PermissionResult::approve_once(), Arc::clone(&permission_requests), )), ) @@ -207,7 +207,9 @@ async fn one_client_rejects_permission_and_both_see_the_result() { .create_session( SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) - .with_permission_handler(permission_handler(PermissionResult::Denied)), + .with_permission_handler(permission_handler(PermissionResult::reject( + None, + ))), ) .await .expect("create session"); diff --git a/rust/tests/e2e/multi_client_commands_elicitation.rs b/rust/tests/e2e/multi_client_commands_elicitation.rs index 038209c2b..be096bfa6 100644 --- a/rust/tests/e2e/multi_client_commands_elicitation.rs +++ b/rust/tests/e2e/multi_client_commands_elicitation.rs @@ -244,7 +244,7 @@ impl PermissionHandler for ElicitationApproveHandler { _request_id: RequestId, _data: github_copilot_sdk::PermissionRequestData, ) -> PermissionResult { - PermissionResult::Approved + PermissionResult::approve_once() } } diff --git a/rust/tests/e2e/permissions.rs b/rust/tests/e2e/permissions.rs index cecf04269..3ad01193f 100644 --- a/rust/tests/e2e/permissions.rs +++ b/rust/tests/e2e/permissions.rs @@ -45,9 +45,14 @@ async fn should_work_with_approve_all_permission_handler() { #[tokio::test] async fn should_handle_permission_handler_errors_gracefully() { - let result = PermissionResult::UserNotAvailable; - - assert!(matches!(result, PermissionResult::UserNotAvailable)); + let result = PermissionResult::user_not_available(); + + assert!(matches!( + result, + PermissionResult::Decision( + github_copilot_sdk::types::PermissionDecision::UserNotAvailable(_) + ) + )); } #[tokio::test] @@ -77,7 +82,7 @@ async fn should_deny_permission_when_handler_returns_denied() { SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) .with_permission_handler(Arc::new(StaticPermissionHandler::new( - PermissionResult::Denied, + PermissionResult::reject(None), ))), ) .await @@ -127,7 +132,7 @@ async fn should_deny_tool_operations_when_handler_explicitly_denies() { SessionConfig::default() .with_github_token(DEFAULT_TEST_TOKEN) .with_permission_handler(Arc::new(StaticPermissionHandler::new( - PermissionResult::UserNotAvailable, + PermissionResult::user_not_available(), ))), ) .await @@ -273,7 +278,7 @@ async fn should_deny_tool_operations_when_handler_explicitly_denies_after_resume ResumeSessionConfig::new(session_id) .with_github_token(DEFAULT_TEST_TOKEN) .with_permission_handler(Arc::new(StaticPermissionHandler::new( - PermissionResult::UserNotAvailable, + PermissionResult::user_not_available(), ))), ) .await @@ -653,7 +658,7 @@ impl PermissionHandler for RecordingPermissionHandler { data: PermissionRequestData, ) -> PermissionResult { let _ = self.request_tx.send(data); - PermissionResult::Approved + PermissionResult::approve_once() } } @@ -689,7 +694,7 @@ impl PermissionHandler for AsyncPermissionHandler { ) -> PermissionResult { tokio::task::yield_now().await; let _ = self.request_tx.send(data); - PermissionResult::Approved + PermissionResult::approve_once() } } @@ -712,6 +717,6 @@ impl PermissionHandler for SlowPermissionHandler { if let Some(release_rx) = self.release_rx.lock().await.take() { let _ = release_rx.await; } - PermissionResult::Approved + PermissionResult::approve_once() } } diff --git a/rust/tests/e2e/tools.rs b/rust/tests/e2e/tools.rs index 327058a4d..85d15b571 100644 --- a/rust/tests/e2e/tools.rs +++ b/rust/tests/e2e/tools.rs @@ -202,7 +202,7 @@ async fn skippermission_sent_in_tool_definition() { let (permission_tx, mut permission_rx) = mpsc::unbounded_channel(); let handler = Arc::new(RecordingPermissionHandler { permission_tx, - decision: PermissionResult::Denied, + decision: PermissionResult::reject(None), }); let __perm = handler; let tools = vec![safe_lookup_tool()]; @@ -252,7 +252,7 @@ async fn invokes_custom_tool_with_permission_handler() { let (permission_tx, mut permission_rx) = mpsc::unbounded_channel(); let handler = Arc::new(RecordingPermissionHandler { permission_tx, - decision: PermissionResult::Approved, + decision: PermissionResult::approve_once(), }); let __perm = handler; let tools = vec![encrypt_string_tool()]; @@ -296,7 +296,7 @@ async fn denies_custom_tool_when_permission_denied() { let (permission_tx, _permission_rx) = mpsc::unbounded_channel(); let handler = Arc::new(RecordingPermissionHandler { permission_tx, - decision: PermissionResult::Denied, + decision: PermissionResult::reject(None), }); let __perm = handler; let tools = vec![tracked_encrypt_string_tool(call_tx)]; diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 279834852..e8977aceb 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -1189,7 +1189,7 @@ async fn permission_request_dispatches_to_handler() { _request_id: RequestId, _data: PermissionRequestData, ) -> PermissionResult { - PermissionResult::Denied + PermissionResult::reject(None) } } diff --git a/test/scenarios/callbacks/permissions/rust/src/main.rs b/test/scenarios/callbacks/permissions/rust/src/main.rs index 287cfd9f1..69c0037a6 100644 --- a/test/scenarios/callbacks/permissions/rust/src/main.rs +++ b/test/scenarios/callbacks/permissions/rust/src/main.rs @@ -29,7 +29,7 @@ impl PermissionHandler for PermissionLogger { .unwrap_or("") .to_string(); self.log.lock().await.push(format!("approved:{tool_name}")); - PermissionResult::Approved + PermissionResult::approve_once() } } diff --git a/test/scenarios/callbacks/user-input/rust/src/main.rs b/test/scenarios/callbacks/user-input/rust/src/main.rs index 9f805a367..348248389 100644 --- a/test/scenarios/callbacks/user-input/rust/src/main.rs +++ b/test/scenarios/callbacks/user-input/rust/src/main.rs @@ -24,7 +24,7 @@ impl PermissionHandler for InputResponder { _request_id: RequestId, _data: PermissionRequestData, ) -> PermissionResult { - PermissionResult::Approved + PermissionResult::approve_once() } } From 8b51c9868455d8a729ccd22fcd878bb8a2430376 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 16:21:34 +0100 Subject: [PATCH 07/14] Format fixes: gofmt import order + prettier MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI flagged formatting issues introduced by the previous .NET/Go/Rust PermissionDecision refactor commits: - Three Go test files had `rpc` imported above `testharness` — gofmt prefers alphabetical-within-group order. - nodejs/test/e2e/client_options.e2e.test.ts had a prettier wrap miss from the earlier scenarios cleanup commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- go/internal/e2e/multi_client_e2e_test.go | 2 +- go/internal/e2e/suspend_e2e_test.go | 2 +- go/internal/e2e/tools_e2e_test.go | 2 +- nodejs/test/e2e/client_options.e2e.test.ts | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/go/internal/e2e/multi_client_e2e_test.go b/go/internal/e2e/multi_client_e2e_test.go index 2a0b15c4f..a5c852bc8 100644 --- a/go/internal/e2e/multi_client_e2e_test.go +++ b/go/internal/e2e/multi_client_e2e_test.go @@ -10,8 +10,8 @@ import ( "time" copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/rpc" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" ) func TestMultiClientE2E(t *testing.T) { diff --git a/go/internal/e2e/suspend_e2e_test.go b/go/internal/e2e/suspend_e2e_test.go index 98a15fb44..672481a4f 100644 --- a/go/internal/e2e/suspend_e2e_test.go +++ b/go/internal/e2e/suspend_e2e_test.go @@ -8,8 +8,8 @@ import ( "time" copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/rpc" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" ) const suspendTimeout = 60 * time.Second diff --git a/go/internal/e2e/tools_e2e_test.go b/go/internal/e2e/tools_e2e_test.go index 6f9510e3d..621f7758d 100644 --- a/go/internal/e2e/tools_e2e_test.go +++ b/go/internal/e2e/tools_e2e_test.go @@ -9,8 +9,8 @@ import ( "testing" copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/rpc" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" ) func TestToolsE2E(t *testing.T) { diff --git a/nodejs/test/e2e/client_options.e2e.test.ts b/nodejs/test/e2e/client_options.e2e.test.ts index 8c23ab046..cd67cf672 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -154,7 +154,6 @@ describe("Client options", async () => { } }); - const session = await client.createSession({ onPermissionRequest: approveAll }); expect(session.sessionId).toMatch(/^[a-f0-9-]+$/); From 13f711670b18d599fd4eeaabd3280cbd53603453 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 16:36:09 +0100 Subject: [PATCH 08/14] Drop protocol v2 support: bump MIN to 3 + delete v2 RPC handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the minimum supported wire protocol version from 2 to 3 across TypeScript, Python, .NET, Go, and Rust (Java intentionally left at 2 — out of scope for this PR). Before this change every SDK registered v2-only RPC handlers for ``tool.call`` and ``permission.request`` alongside the v3 broadcast-event model. The handlers were dead code on a v3 server (the server only ever sends broadcast events) but were kept "just in case" the SDK ever connected to a v2 server. Now that ``MIN_PROTOCOL_VERSION = 3``, a v2 server is rejected at the ``connect`` handshake before any session-level RPC can fire, so the v2 adapters can be deleted outright. Deletions per SDK: - **.NET** (``Client.cs``): - ``OnToolCallV2`` and ``OnPermissionRequestV2`` RPC handler methods - ``ToolCallResponseV2`` and ``PermissionRequestResponseV2`` records - Their ``[JsonSerializable]`` entries - ``NoResultPermissionDirectRpcErrorMessage`` constant (no longer reachable) - **TypeScript** (``client.ts``, ``session.ts``): - ``handleToolCallRequestV2`` / ``handlePermissionRequestV2`` private methods on ``CopilotClient`` - ``normalizeToolResultV2`` / ``isToolResultObject`` helpers - ``CopilotSession._handlePermissionRequestV2`` - ``NO_RESULT_PERMISSION_V2_ERROR`` constant - Unused ``ToolCallRequestPayload`` / ``ToolCallResponsePayload`` / ``ToolResultObject`` imports in ``client.ts`` - **Python** (``client.py``): - ``_handle_tool_call_request_v2`` and ``_handle_permission_request_v2`` methods (both create_session and resume_session paths) - The whole "Protocol v2 backward-compatibility adapters" section - ``_NO_RESULT_PERMISSION_V2_ERROR`` constant - ``test_v2_permission_adapter_rejects_no_result`` unit test - **Go** (``client.go``): - ``handleToolCallRequestV2`` and ``handlePermissionRequestV2`` methods - ``toolCallRequestV2`` / ``toolCallResponseV2`` / ``permissionRequestV2`` / ``permissionResponseV2`` payload types - ``noResultPermissionDirectRpcError`` constant - **Rust** (``session.rs``, ``lib.rs``): - ``permission.request`` direct-RPC match arm in the session router - ``direct_permission_payload`` helper + its unit tests What's preserved: - ``MIN_PROTOCOL_VERSION`` constant in each SDK (now ``= 3``) - The handshake check that rejects servers reporting older versions - ``negotiated_protocol_version`` field on the client (no longer branched on anywhere, but harmless and may grow back if we add a v4 later) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 122 +------------------------------ go/client.go | 122 +------------------------------ nodejs/src/client.ts | 154 +-------------------------------------- nodejs/src/session.ts | 33 --------- python/copilot/client.py | 130 +-------------------------------- python/test_client.py | 26 +------ rust/src/lib.rs | 2 +- rust/src/session.rs | 102 +------------------------- 8 files changed, 12 insertions(+), 679 deletions(-) diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 268381618..73a3bf8fd 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -53,13 +53,10 @@ namespace GitHub.Copilot; /// public sealed partial class CopilotClient : IDisposable, IAsyncDisposable { - internal const string NoResultPermissionDirectRpcErrorMessage = - "Permission handlers cannot return PermissionDecision.NoResult() for this permission request."; - /// /// Minimum protocol version this SDK can communicate with. /// - private const int MinProtocolVersion = 2; + private const int MinProtocolVersion = 3; /// /// Provides a thread-safe collection of active Copilot sessions, indexed by session identifier. @@ -1610,12 +1607,6 @@ private async Task ConnectToServerAsync(Process? cliProcess, string? var handler = new RpcHandler(this); rpc.SetLocalRpcMethod("session.event", handler.OnSessionEvent); rpc.SetLocalRpcMethod("session.lifecycle", handler.OnSessionLifecycle); - // Protocol v3 servers send tool calls / permission requests as broadcast events. - // Protocol v2 servers use the older tool.call / permission.request RPC model. - // We always register v2 adapters because handlers are set up before version - // negotiation; a v3 server will simply never send these requests. - rpc.SetLocalRpcMethod("tool.call", handler.OnToolCallV2); - rpc.SetLocalRpcMethod("permission.request", handler.OnPermissionRequestV2); rpc.SetLocalRpcMethod("userInput.request", handler.OnUserInputRequest); rpc.SetLocalRpcMethod("exitPlanMode.request", handler.OnExitPlanModeRequest); rpc.SetLocalRpcMethod("autoModeSwitch.request", handler.OnAutoModeSwitchRequest); @@ -1799,108 +1790,6 @@ public async ValueTask OnSystemMessageTransfo var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); return await session.HandleSystemMessageTransformAsync(sections); } - - // Protocol v2 backward-compatibility adapters - - public async ValueTask OnToolCallV2(string sessionId, - string toolCallId, - string toolName, - object? arguments, - string? traceparent = null, - string? tracestate = null) - { - using var _ = TelemetryHelpers.RestoreTraceContext(traceparent, tracestate); - - var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}"); - if (session.GetTool(toolName) is not { } tool) - { - // Support for not providing the tool handler is only available in the v3+ model. - // For v2, it must have been provided. - return new ToolCallResponseV2(new ToolResultObject - { - TextResultForLlm = $"Tool '{toolName}' is not supported.", - ResultType = "failure", - Error = $"tool '{toolName}' not supported" - }); - } - - try - { - var invocation = new ToolInvocation - { - SessionId = sessionId, - ToolCallId = toolCallId, - ToolName = toolName, - Arguments = arguments - }; - - var aiFunctionArgs = new AIFunctionArguments - { - Context = new Dictionary - { - [typeof(ToolInvocation)] = invocation - } - }; - - if (arguments is not null) - { - if (arguments is not JsonElement incomingJsonArgs) - { - throw new InvalidOperationException($"Incoming arguments must be a {nameof(JsonElement)}; received {arguments.GetType().Name}"); - } - - foreach (var prop in incomingJsonArgs.EnumerateObject()) - { - aiFunctionArgs[prop.Name] = prop.Value; - } - } - - var toolTimestamp = Stopwatch.GetTimestamp(); - var result = await tool.InvokeAsync(aiFunctionArgs); - LoggingHelpers.LogTiming(client._logger, LogLevel.Debug, null, - "RpcHandler.OnToolCallV2 tool dispatch. Elapsed={Elapsed}, SessionId={SessionId}, ToolCallId={ToolCallId}, Tool={ToolName}", - toolTimestamp, - sessionId, - toolCallId, - toolName); - - var toolResultObject = ToolResultObject.ConvertFromInvocationResult(result, tool.JsonSerializerOptions); - return new ToolCallResponseV2(toolResultObject); - } - catch (Exception ex) - { - return new ToolCallResponseV2(new ToolResultObject - { - TextResultForLlm = "Invoking this tool produced an error. Detailed information is not available.", - ResultType = "failure", - Error = ex.Message - }); - } - } - - public async ValueTask OnPermissionRequestV2(string sessionId, JsonElement permissionRequest) - { - var session = client.GetSession(sessionId) - ?? throw new ArgumentException($"Unknown session {sessionId}"); - - try - { - var decision = await session.HandlePermissionRequestAsync(permissionRequest); - if (decision is PermissionDecisionNoResult) - { - throw new InvalidOperationException(NoResultPermissionDirectRpcErrorMessage); - } - return new PermissionRequestResponseV2(decision); - } - catch (InvalidOperationException ex) when (ex.Message == NoResultPermissionDirectRpcErrorMessage) - { - throw; - } - catch (Exception) - { - return new PermissionRequestResponseV2(PermissionDecision.UserNotAvailable()); - } - } } private class Connection( @@ -2071,13 +1960,6 @@ internal record AutoModeSwitchRequestResponse( internal record HooksInvokeResponse( object? Output); - // Protocol v2 backward-compatibility response types - internal record ToolCallResponseV2( - ToolResultObject Result); - - internal record PermissionRequestResponseV2( - PermissionDecision Result); - [JsonSourceGenerationOptions( JsonSerializerDefaults.Web, AllowOutOfOrderMetadataProperties = true, @@ -2100,7 +1982,6 @@ internal record PermissionRequestResponseV2( [JsonSerializable(typeof(GetSessionMetadataRequest))] [JsonSerializable(typeof(GetSessionMetadataResponse))] [JsonSerializable(typeof(ModelCapabilitiesOverride))] - [JsonSerializable(typeof(PermissionRequestResponseV2))] [JsonSerializable(typeof(ProviderConfig))] [JsonSerializable(typeof(ResumeSessionRequest))] [JsonSerializable(typeof(ResumeSessionResponse))] @@ -2111,7 +1992,6 @@ internal record PermissionRequestResponseV2( [JsonSerializable(typeof(SystemMessageConfig))] [JsonSerializable(typeof(SystemMessageTransformRpcResponse))] [JsonSerializable(typeof(CommandWireDefinition))] - [JsonSerializable(typeof(ToolCallResponseV2))] [JsonSerializable(typeof(ToolDefinition))] [JsonSerializable(typeof(ToolResultAIContent))] [JsonSerializable(typeof(ToolResultObject))] diff --git a/go/client.go b/go/client.go index 046a4763f..9491eb199 100644 --- a/go/client.go +++ b/go/client.go @@ -52,8 +52,6 @@ import ( "github.com/github/copilot-sdk/go/rpc" ) -const noResultPermissionDirectRpcError = "permission handlers cannot return PermissionDecisionNoResult for this permission request" - func validateSessionFsConfig(config *SessionFsConfig) error { if config == nil { return nil @@ -1384,7 +1382,7 @@ func (c *Client) ListModels(ctx context.Context) ([]ModelInfo, error) { } // minProtocolVersion is the minimum protocol version this SDK can communicate with. -const minProtocolVersion = 2 +const minProtocolVersion = 3 // verifyProtocolVersion sends the `connect` handshake (carrying the optional token) and // verifies the server's protocol version. Falls back to `ping` against legacy servers @@ -1740,15 +1738,9 @@ func (c *Client) connectViaTcp(ctx context.Context) error { } // setupNotificationHandler configures handlers for session events and RPC requests. -// Protocol v3 servers send tool calls and permission requests as broadcast session events. -// Protocol v2 servers use the older tool.call / permission.request RPC model. -// We always register v2 adapters because handlers are set up before version negotiation; -// a v3 server will simply never send these requests. func (c *Client) setupNotificationHandler() { c.client.SetRequestHandler("session.event", jsonrpc2.NotificationHandlerFor(c.handleSessionEvent)) c.client.SetRequestHandler("session.lifecycle", jsonrpc2.NotificationHandlerFor(c.handleLifecycleEvent)) - c.client.SetRequestHandler("tool.call", jsonrpc2.RequestHandlerFor(c.handleToolCallRequestV2)) - c.client.SetRequestHandler("permission.request", jsonrpc2.RequestHandlerFor(c.handlePermissionRequestV2)) c.client.SetRequestHandler("userInput.request", jsonrpc2.RequestHandlerFor(c.handleUserInputRequest)) c.client.SetRequestHandler("exitPlanMode.request", jsonrpc2.RequestHandlerFor(c.handleExitPlanModeRequest)) c.client.SetRequestHandler("autoModeSwitch.request", jsonrpc2.RequestHandlerFor(c.handleAutoModeSwitchRequest)) @@ -1902,115 +1894,3 @@ func (c *Client) handleSystemMessageTransform(req systemMessageTransformRequest) } return resp, nil } - -// ======================================================================== -// Protocol v2 backward-compatibility adapters -// ======================================================================== - -// toolCallRequestV2 is the v2 RPC request payload for tool.call. -type toolCallRequestV2 struct { - SessionID string `json:"sessionId"` - ToolCallID string `json:"toolCallId"` - ToolName string `json:"toolName"` - Arguments any `json:"arguments"` - Traceparent string `json:"traceparent,omitempty"` - Tracestate string `json:"tracestate,omitempty"` -} - -// toolCallResponseV2 is the v2 RPC response payload for tool.call. -type toolCallResponseV2 struct { - Result ToolResult `json:"result"` -} - -// permissionRequestV2 is the v2 RPC request payload for permission.request. -type permissionRequestV2 struct { - SessionID string `json:"sessionId"` - Request PermissionRequest `json:"permissionRequest"` -} - -// permissionResponseV2 is the v2 RPC response payload for permission.request. -type permissionResponseV2 struct { - Result rpc.PermissionDecision `json:"result"` -} - -// handleToolCallRequestV2 handles a v2-style tool.call RPC request from the server. -func (c *Client) handleToolCallRequestV2(req toolCallRequestV2) (*toolCallResponseV2, *jsonrpc2.Error) { - if req.SessionID == "" || req.ToolCallID == "" || req.ToolName == "" { - return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid tool call payload"} - } - - c.sessionsMux.Lock() - session, ok := c.sessions[req.SessionID] - c.sessionsMux.Unlock() - if !ok { - return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("unknown session %s", req.SessionID)} - } - - handler, ok := session.getToolHandler(req.ToolName) - if !ok { - return &toolCallResponseV2{Result: ToolResult{ - TextResultForLLM: fmt.Sprintf("Tool '%s' is not supported by this client instance.", req.ToolName), - ResultType: "failure", - Error: fmt.Sprintf("tool '%s' not supported", req.ToolName), - ToolTelemetry: map[string]any{}, - }}, nil - } - - ctx := contextWithTraceParent(context.Background(), req.Traceparent, req.Tracestate) - - invocation := ToolInvocation{ - SessionID: req.SessionID, - ToolCallID: req.ToolCallID, - ToolName: req.ToolName, - Arguments: req.Arguments, - TraceContext: ctx, - } - - result, err := handler(invocation) - if err != nil { - return &toolCallResponseV2{Result: ToolResult{ - TextResultForLLM: "Invoking this tool produced an error. Detailed information is not available.", - ResultType: "failure", - Error: err.Error(), - ToolTelemetry: map[string]any{}, - }}, nil - } - - return &toolCallResponseV2{Result: result}, nil -} - -// handlePermissionRequestV2 handles a v2-style permission.request RPC request from the server. -func (c *Client) handlePermissionRequestV2(req permissionRequestV2) (*permissionResponseV2, *jsonrpc2.Error) { - if req.SessionID == "" { - return nil, &jsonrpc2.Error{Code: -32602, Message: "invalid permission request payload"} - } - - c.sessionsMux.Lock() - session, ok := c.sessions[req.SessionID] - c.sessionsMux.Unlock() - if !ok { - return nil, &jsonrpc2.Error{Code: -32602, Message: fmt.Sprintf("unknown session %s", req.SessionID)} - } - - handler := session.getPermissionHandler() - if handler == nil { - return &permissionResponseV2{Result: &rpc.PermissionDecisionUserNotAvailable{}}, nil - } - - invocation := PermissionInvocation{ - SessionID: session.SessionID, - } - - decision, err := handler(req.Request, invocation) - if err != nil { - return &permissionResponseV2{Result: &rpc.PermissionDecisionUserNotAvailable{}}, nil - } - if _, isNoResult := decision.(*rpc.PermissionDecisionNoResult); isNoResult { - return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionDirectRpcError} - } - if _, isNoResult := decision.(rpc.PermissionDecisionNoResult); isNoResult { - return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionDirectRpcError} - } - - return &permissionResponseV2{Result: decision}, nil -} diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index abfa13a22..21563d598 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -32,7 +32,7 @@ import { registerClientSessionApiHandlers, } from "./generated/rpc.js"; import { getSdkProtocolVersion } from "./sdkProtocolVersion.js"; -import { CopilotSession, NO_RESULT_PERMISSION_V2_ERROR } from "./session.js"; +import { CopilotSession } from "./session.js"; import { createSessionFsAdapter, type SessionFsProvider } from "./sessionFsProvider.js"; import { getTraceContext } from "./telemetry.js"; import type { @@ -61,9 +61,6 @@ import type { SystemMessageCustomizeConfig, TelemetryConfig, Tool, - ToolCallRequestPayload, - ToolCallResponsePayload, - ToolResultObject, TraceContextProvider, TypedSessionLifecycleHandler, } from "./types.js"; @@ -73,7 +70,7 @@ import { defaultJoinSessionPermissionHandler } from "./types.js"; * Minimum protocol version this SDK can communicate with. * Servers reporting a version below this are rejected. */ -const MIN_PROTOCOL_VERSION = 2; +const MIN_PROTOCOL_VERSION = 3; /** * Check if value is a Zod schema (has toJSONSchema method) @@ -1839,25 +1836,6 @@ export class CopilotClient { this.handleSessionLifecycleNotification(notification); }); - // Protocol v3 servers send tool calls and permission requests as broadcast events - // (external_tool.requested / permission.requested) handled in CopilotSession._dispatchEvent. - // Protocol v2 servers use the older tool.call / permission.request RPC model instead. - // We always register v2 adapters because handlers are set up before version negotiation; - // a v3 server will simply never send these requests. - this.connection.onRequest( - "tool.call", - async (params: ToolCallRequestPayload): Promise => - await this.handleToolCallRequestV2(params) - ); - - this.connection.onRequest( - "permission.request", - async (params: { - sessionId: string; - permissionRequest: unknown; - }): Promise<{ result: unknown }> => await this.handlePermissionRequestV2(params) - ); - this.connection.onRequest( "userInput.request", async (params: { @@ -2105,132 +2083,4 @@ export class CopilotClient { return await session._handleSystemMessageTransform(params.sections); } - - // ======================================================================== - // Protocol v2 backward-compatibility adapters - // ======================================================================== - - /** - * Handles a v2-style tool.call RPC request from the server. - * Looks up the session and tool handler, executes it, and returns the result - * in the v2 response format. - */ - private async handleToolCallRequestV2( - params: ToolCallRequestPayload - ): Promise { - if ( - !params || - typeof params.sessionId !== "string" || - typeof params.toolCallId !== "string" || - typeof params.toolName !== "string" - ) { - throw new Error("Invalid tool call payload"); - } - - const session = this.sessions.get(params.sessionId); - if (!session) { - throw new Error(`Unknown session ${params.sessionId}`); - } - - const handler = session.getToolHandler(params.toolName); - if (!handler) { - return { - result: { - textResultForLlm: `Tool '${params.toolName}' is not supported by this client instance.`, - resultType: "failure", - error: `tool '${params.toolName}' not supported`, - toolTelemetry: {}, - }, - }; - } - - try { - const traceparent = (params as { traceparent?: string }).traceparent; - const tracestate = (params as { tracestate?: string }).tracestate; - const invocation = { - sessionId: params.sessionId, - toolCallId: params.toolCallId, - toolName: params.toolName, - arguments: params.arguments, - traceparent, - tracestate, - }; - const result = await handler(params.arguments, invocation); - return { result: this.normalizeToolResultV2(result) }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - result: { - textResultForLlm: - "Invoking this tool produced an error. Detailed information is not available.", - resultType: "failure", - error: message, - toolTelemetry: {}, - }, - }; - } - } - - /** - * Handles a v2-style permission.request RPC request from the server. - */ - private async handlePermissionRequestV2(params: { - sessionId: string; - permissionRequest: unknown; - }): Promise<{ result: unknown }> { - if (!params || typeof params.sessionId !== "string" || !params.permissionRequest) { - throw new Error("Invalid permission request payload"); - } - - const session = this.sessions.get(params.sessionId); - if (!session) { - throw new Error(`Session not found: ${params.sessionId}`); - } - - try { - const result = await session._handlePermissionRequestV2(params.permissionRequest); - return { result }; - } catch (error) { - if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) { - throw error; - } - return { - result: { - kind: "user-not-available", - }, - }; - } - } - - private normalizeToolResultV2(result: unknown): ToolResultObject { - if (result === undefined || result === null) { - return { - textResultForLlm: "Tool returned no result", - resultType: "failure", - error: "tool returned no result", - toolTelemetry: {}, - }; - } - - if (this.isToolResultObject(result)) { - return result; - } - - const textResult = typeof result === "string" ? result : JSON.stringify(result); - return { - textResultForLlm: textResult, - resultType: "success", - toolTelemetry: {}, - }; - } - - private isToolResultObject(value: unknown): value is ToolResultObject { - return ( - typeof value === "object" && - value !== null && - "textResultForLlm" in value && - typeof (value as ToolResultObject).textResultForLlm === "string" && - "resultType" in value - ); - } } diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 4baf35c3e..2ca285c0f 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -50,10 +50,6 @@ import type { UserInputResponse, } from "./types.js"; -/** @internal */ -export const NO_RESULT_PERMISSION_V2_ERROR = - "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."; - /** * Convert a raw hook input received over the wire into its public-facing shape. * Currently this only deserializes the numeric Unix-ms `timestamp` field on @@ -907,35 +903,6 @@ export class CopilotSession { return { sections: result }; } - /** - * Handles a permission request in the v2 protocol format (synchronous RPC). - * Used as a back-compat adapter when connected to a v2 server. - * - * @param request - The permission request data from the CLI - * @returns A promise that resolves with the permission decision - * @internal This method is for internal use by the SDK. - */ - async _handlePermissionRequestV2(request: unknown): Promise { - if (!this.permissionHandler) { - return { kind: "user-not-available" }; - } - - try { - const result = await this.permissionHandler(request as PermissionRequest, { - sessionId: this.sessionId, - }); - if (result.kind === "no-result") { - throw new Error(NO_RESULT_PERMISSION_V2_ERROR); - } - return result; - } catch (error) { - if (error instanceof Error && error.message === NO_RESULT_PERMISSION_V2_ERROR) { - throw error; - } - return { kind: "user-not-available" }; - } - } - /** * Handles a user input request from the Copilot CLI. * diff --git a/python/copilot/client.py b/python/copilot/client.py index b2f0b6d22..b65f62481 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -35,11 +35,10 @@ from ._diagnostics import log_timing from ._jsonrpc import JsonRpcClient, JsonRpcError, ProcessExitedError from ._sdk_protocol_version import get_sdk_protocol_version -from ._telemetry import get_trace_context, trace_context +from ._telemetry import get_trace_context from .generated.rpc import ( ClientSessionApiHandlers, ConnectRequest, - PermissionDecisionUserNotAvailable, RemoteSessionMode, ServerRpc, _InternalServerRpc, @@ -48,7 +47,6 @@ ) from .generated.session_events import ( SessionEvent, - _load_PermissionRequest, session_event_from_dict, ) from .session import ( @@ -62,7 +60,6 @@ ExitPlanModeHandler, InfiniteSessionConfig, MCPServerConfig, - PermissionNoResult, ProviderConfig, ReasoningEffort, SectionTransformFn, @@ -73,7 +70,7 @@ _PermissionHandlerFn, ) from .session_fs_provider import SessionFsProvider, create_session_fs_adapter -from .tools import Tool, ToolInvocation, ToolResult +from .tools import Tool logger = logging.getLogger(__name__) @@ -939,13 +936,9 @@ def _session_lifecycle_event_from_dict(data: dict) -> SessionLifecycleEvent: HandlerUnsubcribe = Callable[[], None] -_NO_RESULT_PERMISSION_V2_ERROR = ( - "Permission handlers cannot return 'no-result' when connected to a protocol v2 server." -) - # Minimum protocol version this SDK can communicate with. # Servers reporting a version below this are rejected. -_MIN_PROTOCOL_VERSION = 2 +_MIN_PROTOCOL_VERSION = 3 def _get_bundled_cli_path() -> str | None: @@ -2984,12 +2977,6 @@ def handle_notification(method: str, params: dict): self._dispatch_lifecycle_event(lifecycle_event) self._client.set_notification_handler(handle_notification) - # Protocol v3 servers send tool calls / permission requests as broadcast events. - # Protocol v2 servers use the older tool.call / permission.request RPC model. - # We always register v2 adapters because handlers are set up before version - # negotiation; a v3 server will simply never send these requests. - self._client.set_request_handler("tool.call", self._handle_tool_call_request_v2) - self._client.set_request_handler("permission.request", self._handle_permission_request_v2) self._client.set_request_handler("userInput.request", self._handle_user_input_request) self._client.set_request_handler( "exitPlanMode.request", self._handle_exit_plan_mode_request @@ -3109,11 +3096,6 @@ def handle_notification(method: str, params: dict): self._dispatch_lifecycle_event(lifecycle_event) self._client.set_notification_handler(handle_notification) - # Protocol v3 servers send tool calls / permission requests as broadcast events. - # Protocol v2 servers use the older tool.call / permission.request RPC model. - # We always register v2 adapters; a v3 server will simply never send these requests. - self._client.set_request_handler("tool.call", self._handle_tool_call_request_v2) - self._client.set_request_handler("permission.request", self._handle_permission_request_v2) self._client.set_request_handler("userInput.request", self._handle_user_input_request) self._client.set_request_handler( "exitPlanMode.request", self._handle_exit_plan_mode_request @@ -3254,109 +3236,3 @@ async def _handle_system_message_transform(self, params: dict) -> dict: raise ValueError(f"unknown session {session_id}") return await session._handle_system_message_transform(sections) - - # ======================================================================== - # Protocol v2 backward-compatibility adapters - # ======================================================================== - - async def _handle_tool_call_request_v2(self, params: dict) -> dict: - """Handle a v2-style tool.call RPC request from the server.""" - session_id = params.get("sessionId") - tool_call_id = params.get("toolCallId") - tool_name = params.get("toolName") - - if not session_id or not tool_call_id or not tool_name: - raise ValueError("invalid tool call payload") - - with self._sessions_lock: - session = self._sessions.get(session_id) - if not session: - raise ValueError(f"unknown session {session_id}") - - handler = session._get_tool_handler(tool_name) - if not handler: - return { - "result": { - "textResultForLlm": ( - f"Tool '{tool_name}' is not supported by this client instance." - ), - "resultType": "failure", - "error": f"tool '{tool_name}' not supported", - "toolTelemetry": {}, - } - } - - arguments = params.get("arguments") - invocation = ToolInvocation( - session_id=session_id, - tool_call_id=tool_call_id, - tool_name=tool_name, - arguments=arguments, - ) - - tp = params.get("traceparent") - ts = params.get("tracestate") - - try: - with trace_context(tp, ts): - handler_start = time.perf_counter() - result = handler(invocation) - if inspect.isawaitable(result): - result = await result - log_timing( - logger, - logging.DEBUG, - "CopilotClient._handle_tool_call_request_v2 tool dispatch", - handler_start, - session_id=session_id, - tool_call_id=tool_call_id, - tool_name=tool_name, - ) - - tool_result: ToolResult = result # type: ignore[assignment] - return { - "result": { - "textResultForLlm": tool_result.text_result_for_llm, - "resultType": tool_result.result_type, - "error": tool_result.error, - "toolTelemetry": tool_result.tool_telemetry or {}, - } - } - except Exception as exc: - return { - "result": { - "textResultForLlm": ( - "Invoking this tool produced an error." - " Detailed information is not available." - ), - "resultType": "failure", - "error": str(exc), - "toolTelemetry": {}, - } - } - - async def _handle_permission_request_v2(self, params: dict) -> dict: - """Handle a v2-style permission.request RPC request from the server.""" - session_id = params.get("sessionId") - permission_request = params.get("permissionRequest") - - if not session_id or not permission_request: - raise ValueError("invalid permission request payload") - - with self._sessions_lock: - session = self._sessions.get(session_id) - if not session: - raise ValueError(f"unknown session {session_id}") - - try: - perm_request = _load_PermissionRequest(permission_request) - result = await session._handle_permission_request(perm_request) - if isinstance(result, PermissionNoResult): - raise ValueError(_NO_RESULT_PERMISSION_V2_ERROR) - return {"result": result.to_dict()} - except ValueError as exc: - if str(exc) == _NO_RESULT_PERMISSION_V2_ERROR: - raise - return {"result": PermissionDecisionUserNotAvailable().to_dict()} - except Exception: # pylint: disable=broad-except - return {"result": PermissionDecisionUserNotAvailable().to_dict()} diff --git a/python/test_client.py b/python/test_client.py index 2ed57657e..14320b3a2 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -22,7 +22,7 @@ ModelLimits, ModelSupports, ) -from copilot.session import PermissionHandler, PermissionNoResult +from copilot.session import PermissionHandler from e2e.testharness import CLI_PATH @@ -47,30 +47,6 @@ async def test_create_session_allows_none_permission_handler(self): finally: await client.force_stop() - @pytest.mark.asyncio - async def test_v2_permission_adapter_rejects_no_result(self): - client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) - await client.start() - try: - session = await client.create_session( - on_permission_request=lambda request, invocation: PermissionNoResult() - ) - with pytest.raises(ValueError, match="protocol v2 server"): - await client._handle_permission_request_v2( - { - "sessionId": session.session_id, - "permissionRequest": { - "kind": "write", - "canOfferSessionApproval": True, - "diff": "", - "fileName": "test.txt", - "intention": "test", - }, - } - ) - finally: - await client.force_stop() - @pytest.mark.asyncio async def test_resume_session_allows_none_permission_handler(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 2e4bcea20..687c8c241 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -69,7 +69,7 @@ pub use sdk_protocol_version::{SDK_PROTOCOL_VERSION, get_sdk_protocol_version}; pub use subscription::{EventSubscription, Lagged, LifecycleSubscription, RecvError}; /// Minimum protocol version this SDK can communicate with. -const MIN_PROTOCOL_VERSION: u32 = 2; +const MIN_PROTOCOL_VERSION: u32 = 3; /// Errors returned by the SDK. #[derive(Debug, thiserror::Error)] diff --git a/rust/src/session.rs b/rust/src/session.rs index 138d1f1a7..b07133eb7 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -1200,8 +1200,8 @@ fn extract_request_id(data: &Value) -> Option { .map(RequestId::new) } -/// Map a [`PermissionResult`] to the `result` payload for the -/// broadcast-event response path (`session.permissions.handlePendingPermissionRequest`). +/// Map a [`PermissionResult`] to the `result` payload sent back to the +/// server via `session.permissions.handlePendingPermissionRequest`. /// /// Returns `None` when the SDK must not send a response. fn notification_permission_payload(result: &PermissionResult) -> Option { @@ -1213,20 +1213,6 @@ fn notification_permission_payload(result: &PermissionResult) -> Option { } } -/// Map a [`PermissionResult`] to the JSON-RPC `result` payload for the -/// direct-RPC path (`permission.request`). -/// -/// Always returns a value. `NoResult` is mapped to `user-not-available` -/// because the JSON-RPC contract requires a reply. -fn direct_permission_payload(result: &PermissionResult) -> Value { - match result { - PermissionResult::Decision(decision) => { - serde_json::to_value(decision).expect("serializing permission decision should succeed") - } - PermissionResult::NoResult => serde_json::json!({ "kind": "user-not-available" }), - } -} - fn tool_failure_result(message: impl Into) -> ToolResult { let message = message.into(); ToolResult::Expanded(ToolResultExpanded { @@ -1912,68 +1898,6 @@ async fn handle_request( let _ = client.send_response(&rpc_response).await; } - "permission.request" => { - let Some(request_id) = request - .params - .as_ref() - .and_then(|p| p.get("requestId")) - .and_then(|v| v.as_str()) - .filter(|s| !s.is_empty()) - else { - warn!("permission.request missing 'requestId' field"); - let rpc_response = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: request.id, - result: None, - error: Some(crate::JsonRpcError { - code: error_codes::INVALID_PARAMS, - message: "missing required field: requestId".to_string(), - data: None, - }), - }; - let _ = client.send_response(&rpc_response).await; - return; - }; - let request_id = RequestId::new(request_id); - let raw_params = request - .params - .as_ref() - .cloned() - .unwrap_or(Value::Object(serde_json::Map::new())); - let data: PermissionRequestData = - serde_json::from_value(raw_params.clone()).unwrap_or(PermissionRequestData { - kind: None, - tool_call_id: None, - extra: raw_params, - }); - - let handler_start = Instant::now(); - let rpc_result = if let Some(permission_handler) = handlers.permission.as_ref() { - let result = permission_handler - .handle(sid.clone(), request_id.clone(), data) - .await; - tracing::debug!( - elapsed_ms = handler_start.elapsed().as_millis(), - session_id = %sid, - request_id = %request_id, - "PermissionHandler::handle dispatch" - ); - direct_permission_payload(&result) - } else { - // Back-compat with v2 servers that still send - // permission.request as a direct RPC: default to - // user-not-available rather than erroring. - serde_json::json!({ "kind": "user-not-available" }) - }; - let rpc_response = JsonRpcResponse { - jsonrpc: "2.0".to_string(), - id: request.id, - result: Some(rpc_result), - error: None, - }; - let _ = client.send_response(&rpc_response).await; - } - "systemMessage.transform" => { let params = request.params.as_ref(); let sections: HashMap = @@ -2103,7 +2027,7 @@ fn inject_transform_sections_resume( mod tests { use serde_json::json; - use super::{direct_permission_payload, notification_permission_payload}; + use super::notification_permission_payload; use crate::handler::PermissionResult; #[test] @@ -2130,24 +2054,4 @@ mod tests { Some(json!({ "kind": "user-not-available" })) ); } - - #[test] - fn direct_payload_maps_no_result_to_user_not_available() { - assert_eq!( - direct_permission_payload(&PermissionResult::NoResult), - json!({ "kind": "user-not-available" }) - ); - } - - #[test] - fn direct_payload_serializes_decisions() { - assert_eq!( - direct_permission_payload(&PermissionResult::approve_once()), - json!({ "kind": "approve-once" }) - ); - assert_eq!( - direct_permission_payload(&PermissionResult::reject(None)), - json!({ "kind": "reject" }) - ); - } } From 712eaeff4e882086da04e1d3515d20dc20a0081d Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 16:45:27 +0100 Subject: [PATCH 09/14] Fix lint + clean up v2-direct-RPC tests - `nodejs/src/session.ts` had an unused `PermissionRequestResult` import after the v2 drop. eslint flagged it (`no-unused-vars`). - `nodejs/test/client.test.ts` had a leftover test for the deleted `handlePermissionRequestV2` v2 adapter. Delete the test. - `rust/tests/session_test.rs` had two tests that drove the deleted `permission.request` direct-RPC arm (`permission_request_dispatches_to_handler`, `approve_all_handler_approves_permission`). Delete the first one and rewrite the second to exercise the broadcast-event path (`permission.requested` event + `handlePendingPermissionRequest` RPC response), which is the only remaining permission flow. - Drop unused `PermissionHandler` / `PermissionResult` imports that the deleted tests left behind. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/src/session.ts | 1 - nodejs/test/client.test.ts | 14 ---------- rust/tests/session_test.rs | 57 +++++++++----------------------------- 3 files changed, 13 insertions(+), 59 deletions(-) diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 2ca285c0f..6f2a002b1 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -28,7 +28,6 @@ import type { MessageOptions, PermissionHandler, PermissionRequest, - PermissionRequestResult, ReasoningEffort, ModelCapabilitiesOverride, SectionTransformFn, diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 5670e13bd..b9a34c214 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -17,20 +17,6 @@ describe("CopilotClient", () => { expect(spy).not.toHaveBeenCalled(); }); - it("throws when a v2 permission handler returns no-result", async () => { - const session = new CopilotSession("session-1", {} as any); - session.registerPermissionHandler(() => ({ kind: "no-result" })); - const client = new CopilotClient(); - (client as any).sessions.set(session.sessionId, session); - - await expect( - (client as any).handlePermissionRequestV2({ - sessionId: session.sessionId, - permissionRequest: { kind: "write" }, - }) - ).rejects.toThrow(/protocol v2 server/); - }); - it("forwards clientName in session.create request", async () => { const client = new CopilotClient(); await client.start(); diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index e8977aceb..5e7acd438 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -8,8 +8,7 @@ use std::time::Duration; use async_trait::async_trait; use github_copilot_sdk::handler::{ ApproveAllHandler, AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, - ExitPlanModeHandler, ExitPlanModeResult, PermissionHandler, PermissionResult, UserInputHandler, - UserInputResponse, + ExitPlanModeHandler, ExitPlanModeResult, UserInputHandler, UserInputResponse, }; use github_copilot_sdk::types::{ CommandContext, CommandDefinition, CommandHandler, DeliveryMode, ElicitationRequest, @@ -1178,41 +1177,6 @@ async fn elicitation_returns_typed_result() { assert_eq!(result.content.unwrap()["name"], "Octocat"); } -#[tokio::test] -async fn permission_request_dispatches_to_handler() { - struct DenyHandler; - #[async_trait] - impl PermissionHandler for DenyHandler { - async fn handle( - &self, - _session_id: SessionId, - _request_id: RequestId, - _data: PermissionRequestData, - ) -> PermissionResult { - PermissionResult::reject(None) - } - } - - let (_session, mut server) = - create_session_pair_with_config(|cfg| cfg.with_permission_handler(Arc::new(DenyHandler))) - .await; - server - .send_request( - 200, - "permission.request", - serde_json::json!({ - "sessionId": server.session_id, - "requestId": "perm-1", - "kind": "shell", - }), - ) - .await; - - let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); - assert_eq!(response["id"], 200); - assert_eq!(response["result"]["kind"], "reject"); -} - #[tokio::test] async fn user_input_request_dispatches_to_handler() { struct InputHandler; @@ -1455,18 +1419,23 @@ async fn approve_all_handler_approves_permission() { .await; server - .send_request( - 500, - "permission.request", + .send_event( + "permission.requested", serde_json::json!({ - "sessionId": server.session_id, "requestId": "perm-auto", - "kind": "shell", + "sessionId": server.session_id, + "permissionRequest": { "kind": "shell" }, }), ) .await; - let response = timeout(TIMEOUT, server.read_response()).await.unwrap(); - assert_eq!(response["result"]["kind"], "approve-once"); + + let request = timeout(TIMEOUT, server.read_request()).await.unwrap(); + assert_eq!( + request["method"], + "session.permissions.handlePendingPermissionRequest" + ); + assert_eq!(request["params"]["requestId"], "perm-auto"); + assert_eq!(request["params"]["result"]["kind"], "approve-once"); } #[tokio::test] From e7886ede98604bf085b2a4b443f49c4e165fc91a Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 16:48:05 +0100 Subject: [PATCH 10/14] Drop unused PermissionRequestData import in session_test.rs CI's clippy treats unused-imports as a denied warning. The mock handler for the v2 `permission_request_dispatches_to_handler` test was the only consumer of `PermissionRequestData`; remove the import now that the test is gone. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- rust/tests/session_test.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 5e7acd438..5140d5413 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -12,8 +12,8 @@ use github_copilot_sdk::handler::{ }; use github_copilot_sdk::types::{ CommandContext, CommandDefinition, CommandHandler, DeliveryMode, ElicitationRequest, - ElicitationResult, ExitPlanModeData, MessageOptions, PermissionRequestData, RequestId, - SessionConfig, SessionId, Tool, ToolInvocation, ToolResult, + ElicitationResult, ExitPlanModeData, MessageOptions, RequestId, SessionConfig, SessionId, Tool, + ToolInvocation, ToolResult, }; use github_copilot_sdk::{Client, tool}; use serde_json::Value; From ac10d3f11dcd2917a91f13f9ce6da7bf4656e931 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 17:06:26 +0100 Subject: [PATCH 11/14] Fix docs validation: PermissionDecision migration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/custom-agents.md | 16 +++++++++------- docs/features/hooks.md | 20 +++++++++++--------- docs/features/image-input.md | 20 +++++++++++--------- docs/features/remote-sessions.md | 11 ++++++----- docs/features/session-persistence.md | 8 +++++--- docs/features/skills.md | 13 +++++++------ docs/features/steering-and-queueing.md | 13 +++++++------ docs/features/streaming-events.md | 5 +++-- dotnet/src/PermissionDecision.cs | 10 +++++----- scripts/docs-validation/extract.ts | 12 +++++++++--- scripts/docs-validation/validate.ts | 2 +- 11 files changed, 74 insertions(+), 56 deletions(-) diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index 716e80583..bd193c8f1 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -103,6 +103,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -128,8 +129,8 @@ func main() { Prompt: "You are a code editor. Make minimal, surgical changes to files as requested.", }, }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) _ = session @@ -160,8 +161,8 @@ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Prompt: "You are a code editor. Make minimal, surgical changes to files as requested.", }, }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) ``` @@ -198,7 +199,7 @@ await using var session = await client.CreateSessionAsync(new SessionConfig }, }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); ``` @@ -519,6 +520,7 @@ import ( "context" "fmt" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -528,8 +530,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) diff --git a/docs/features/hooks.md b/docs/features/hooks.md index 6ba554a1e..1f6c148f8 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -88,6 +88,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func onSessionStart(input copilot.SessionStartHookInput, inv copilot.HookInvocation) (*copilot.SessionStartHookOutput, error) { @@ -112,8 +113,8 @@ func main() { OnPreToolUse: onPreToolUse, OnPostToolUse: onPostToolUse, }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) _ = session @@ -132,8 +133,8 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{ OnPostToolUse: onPostToolUse, // ... add only the hooks you need }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) ``` @@ -169,7 +170,7 @@ public static class HooksExample OnPostToolUse = onPostToolUse, }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); } } @@ -189,7 +190,7 @@ var session = await client.CreateSessionAsync(new SessionConfig // ... add only the hooks you need }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); ``` @@ -293,6 +294,7 @@ import ( "context" "fmt" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -313,8 +315,8 @@ func main() { return &copilot.PreToolUseHookOutput{PermissionDecision: "allow"}, nil }, }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) _ = session @@ -376,7 +378,7 @@ public static class PermissionControlExample }, }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); } } diff --git a/docs/features/image-input.md b/docs/features/image-input.md index 6f743f1fa..e40f53894 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -101,6 +101,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -110,8 +111,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -136,8 +137,8 @@ client.Start(ctx) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -171,7 +172,7 @@ public static class ImageInputExample { Model = "gpt-4.1", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); await session.SendAsync(new MessageOptions @@ -199,7 +200,7 @@ await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-4.1", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); await session.SendAsync(new MessageOptions @@ -321,6 +322,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -330,8 +332,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -385,7 +387,7 @@ public static class BlobAttachmentExample { Model = "gpt-4.1", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); var base64ImageData = "..."; diff --git a/docs/features/remote-sessions.md b/docs/features/remote-sessions.md index 30965a503..f58238eee 100644 --- a/docs/features/remote-sessions.md +++ b/docs/features/remote-sessions.md @@ -60,8 +60,8 @@ session.on(on_event) client, _ := copilot.NewClient(&copilot.ClientOptions{Remote: true}) session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ WorkingDirectory: "/path/to/github-repo", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -81,7 +81,7 @@ var session = await client.CreateSessionAsync(new SessionConfig { WorkingDirectory = "/path/to/github-repo", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); session.On((SessionEvent e) => @@ -97,7 +97,8 @@ session.On((SessionEvent e) => ```rust -use github_copilot_sdk::{Client, ClientOptions, PermissionRequestResult, SessionConfig}; +use github_copilot_sdk::{Client, ClientOptions, SessionConfig}; +use github_copilot_sdk::handler::PermissionResult; let client = Client::start( ClientOptions::new().with_enable_remote_sessions(true) @@ -105,7 +106,7 @@ let client = Client::start( let session = client.create_session( SessionConfig::new("/path/to/github-repo") .with_permission_handler(|_req, _inv| async { - Ok(PermissionRequestResult::approved()) + Ok(PermissionResult::approve_once()) }), ).await?; diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 5a1987227..5498937ef 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -70,6 +70,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -79,8 +80,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ SessionID: "user-123-task-456", Model: "gpt-5.2-codex", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -173,6 +174,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -212,7 +214,7 @@ public static class ResumeSessionExample var session = await client.ResumeSessionAsync("user-123-task-456", new ResumeSessionConfig { OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); await session.SendAndWaitAsync(new MessageOptions { Prompt = "What did we discuss earlier?" }); diff --git a/docs/features/skills.md b/docs/features/skills.md index 05c5a97a9..0ab71d970 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -91,8 +91,8 @@ func main() { "./skills/code-review", "./skills/documentation", }, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -127,7 +127,7 @@ await using var session = await client.CreateSessionAsync(new SessionConfig "./skills/documentation", }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); // Copilot now has access to skills in those directories @@ -211,6 +211,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -220,8 +221,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ SkillDirectories: []string{"./skills"}, DisabledSkills: []string{"experimental-feature", "deprecated-tool"}, - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) _ = session @@ -256,7 +257,7 @@ public static class SkillsExample SkillDirectories = new List { "./skills" }, DisabledSkills = new List { "experimental-feature", "deprecated-tool" }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); } } diff --git a/docs/features/steering-and-queueing.md b/docs/features/steering-and-queueing.md index 237457585..d255d0392 100644 --- a/docs/features/steering-and-queueing.md +++ b/docs/features/steering-and-queueing.md @@ -118,8 +118,8 @@ func main() { session, err := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) if err != nil { @@ -158,7 +158,7 @@ await using var session = await client.CreateSessionAsync(new SessionConfig { Model = "gpt-4.1", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); // Start a long-running task @@ -301,6 +301,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -310,8 +311,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) @@ -370,7 +371,7 @@ public static class QueueingExample { Model = "gpt-4.1", OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); await session.SendAsync(new MessageOptions diff --git a/docs/features/streaming-events.md b/docs/features/streaming-events.md index ebf7d5e30..9b61108ed 100644 --- a/docs/features/streaming-events.md +++ b/docs/features/streaming-events.md @@ -122,6 +122,7 @@ import ( "context" "fmt" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -131,8 +132,8 @@ func main() { session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ Model: "gpt-4.1", Streaming: copilot.Bool(true), - OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (copilot.PermissionRequestResult, error) { - return copilot.PermissionRequestResult{Kind: copilot.PermissionRequestResultKindApproved}, nil + OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) { + return &rpc.PermissionDecisionApproveOnce{}, nil }, }) diff --git a/dotnet/src/PermissionDecision.cs b/dotnet/src/PermissionDecision.cs index bc447ad87..54e123791 100644 --- a/dotnet/src/PermissionDecision.cs +++ b/dotnet/src/PermissionDecision.cs @@ -29,18 +29,18 @@ public sealed class PermissionDecisionNoResult : PermissionDecision public partial class PermissionDecision { /// Approve this single request. - public static PermissionDecisionApproveOnce ApproveOnce() => new(); + public static PermissionDecision ApproveOnce() => new PermissionDecisionApproveOnce(); /// Reject the request, optionally forwarding feedback to the LLM. - public static PermissionDecisionReject Reject(string? feedback = null) => - new() { Feedback = feedback }; + public static PermissionDecision Reject(string? feedback = null) => + new PermissionDecisionReject { Feedback = feedback }; /// Deny the request because no user is available to confirm it. - public static PermissionDecisionUserNotAvailable UserNotAvailable() => new(); + public static PermissionDecision UserNotAvailable() => new PermissionDecisionUserNotAvailable(); /// /// Decline to respond to this permission request, allowing another /// connected client to answer instead. /// - public static PermissionDecisionNoResult NoResult() => new(); + public static PermissionDecision NoResult() => new PermissionDecisionNoResult(); } diff --git a/scripts/docs-validation/extract.ts b/scripts/docs-validation/extract.ts index 0b0879db1..5bb047cc5 100644 --- a/scripts/docs-validation/extract.ts +++ b/scripts/docs-validation/extract.ts @@ -301,10 +301,13 @@ function wrapCodeForValidation(block: CodeBlock): string { } } - // Always ensure SDK using is present - if (!usings.some(u => u.includes("GitHub.Copilot"))) { + // Always ensure SDK usings are present + if (!usings.some(u => u.includes("GitHub.Copilot;"))) { usings.push("using GitHub.Copilot;"); } + if (!usings.some(u => u.includes("GitHub.Copilot.Rpc"))) { + usings.push("using GitHub.Copilot.Rpc;"); + } // Generate a unique class name based on block location const className = `ValidationClass_${block.file.replace(/[^a-zA-Z0-9]/g, "_")}_${block.line}`; @@ -335,10 +338,13 @@ ${indentedCode} }`; } } else { - // Has structure, but may still need using directive + // Has structure, but may still need using directives if (!code.includes("using GitHub.Copilot;")) { code = "using GitHub.Copilot;\n" + code; } + if (!code.includes("using GitHub.Copilot.Rpc;")) { + code = "using GitHub.Copilot.Rpc;\n" + code; + } } } diff --git a/scripts/docs-validation/validate.ts b/scripts/docs-validation/validate.ts index c1d408c36..bf11baa6e 100644 --- a/scripts/docs-validation/validate.ts +++ b/scripts/docs-validation/validate.ts @@ -286,7 +286,7 @@ async function validateCSharp(): Promise { net8.0 enable enable - CS8019;CS0168;CS0219 + CS8019;CS0168;CS0219;GHCP001 From e3a2ef58e6210faef31305cdaa54d2f877a9376b Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 17:09:16 +0100 Subject: [PATCH 12/14] Suppress GHCP001 in test scenarios Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/scenarios/Directory.Build.props | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 test/scenarios/Directory.Build.props diff --git a/test/scenarios/Directory.Build.props b/test/scenarios/Directory.Build.props new file mode 100644 index 000000000..8350045c9 --- /dev/null +++ b/test/scenarios/Directory.Build.props @@ -0,0 +1,5 @@ + + + $(NoWarn);GHCP001 + + From 0af047803376793d776b749576d8c980f94545e6 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 17:13:02 +0100 Subject: [PATCH 13/14] Fix docs validation: Go imports + C# Rpc using Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/features/custom-agents.md | 1 + docs/features/hooks.md | 2 ++ docs/features/image-input.md | 3 +++ docs/features/session-persistence.md | 2 +- docs/features/skills.md | 3 +++ docs/features/steering-and-queueing.md | 3 +++ scripts/docs-validation/extract.ts | 24 ++++++++++++++---------- 7 files changed, 27 insertions(+), 11 deletions(-) diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index bd193c8f1..5329f163e 100644 --- a/docs/features/custom-agents.md +++ b/docs/features/custom-agents.md @@ -174,6 +174,7 @@ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig diff --git a/docs/features/hooks.md b/docs/features/hooks.md index 1f6c148f8..bd55797dd 100644 --- a/docs/features/hooks.md +++ b/docs/features/hooks.md @@ -147,6 +147,7 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class HooksExample { @@ -350,6 +351,7 @@ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class PermissionControlExample { diff --git a/docs/features/image-input.md b/docs/features/image-input.md index e40f53894..cf2dee518 100644 --- a/docs/features/image-input.md +++ b/docs/features/image-input.md @@ -162,6 +162,7 @@ session.Send(ctx, copilot.MessageOptions{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class ImageInputExample { @@ -194,6 +195,7 @@ public static class ImageInputExample ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig @@ -377,6 +379,7 @@ session.Send(ctx, copilot.MessageOptions{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class BlobAttachmentExample { diff --git a/docs/features/session-persistence.md b/docs/features/session-persistence.md index 5498937ef..3bfff10d0 100644 --- a/docs/features/session-persistence.md +++ b/docs/features/session-persistence.md @@ -174,7 +174,6 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" - "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -204,6 +203,7 @@ session.SendAndWait(ctx, copilot.MessageOptions{Prompt: "What did we discuss ear ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class ResumeSessionExample { diff --git a/docs/features/skills.md b/docs/features/skills.md index 0ab71d970..bac35e39e 100644 --- a/docs/features/skills.md +++ b/docs/features/skills.md @@ -75,6 +75,7 @@ import ( "context" "log" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -116,6 +117,7 @@ func main() { ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig @@ -245,6 +247,7 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class SkillsExample { diff --git a/docs/features/steering-and-queueing.md b/docs/features/steering-and-queueing.md index d255d0392..3b32f678d 100644 --- a/docs/features/steering-and-queueing.md +++ b/docs/features/steering-and-queueing.md @@ -106,6 +106,7 @@ import ( "context" "log" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -152,6 +153,7 @@ func main() { ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; await using var client = new CopilotClient(); await using var session = await client.CreateSessionAsync(new SessionConfig @@ -361,6 +363,7 @@ session.Send(ctx, copilot.MessageOptions{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class QueueingExample { diff --git a/scripts/docs-validation/extract.ts b/scripts/docs-validation/extract.ts index 5bb047cc5..b8d7cb089 100644 --- a/scripts/docs-validation/extract.ts +++ b/scripts/docs-validation/extract.ts @@ -301,11 +301,15 @@ function wrapCodeForValidation(block: CodeBlock): string { } } - // Always ensure SDK usings are present - if (!usings.some(u => u.includes("GitHub.Copilot;"))) { + // Always ensure SDK usings are present. If the snippet already + // declares any GitHub.Copilot using, assume the author curated + // them and don't add others (avoids name ambiguities like + // ModelCapabilities living in both namespaces). + const hasAnyCopilotUsing = usings.some(u => + u.includes("GitHub.Copilot;") || u.includes("GitHub.Copilot."), + ); + if (!hasAnyCopilotUsing) { usings.push("using GitHub.Copilot;"); - } - if (!usings.some(u => u.includes("GitHub.Copilot.Rpc"))) { usings.push("using GitHub.Copilot.Rpc;"); } @@ -338,12 +342,12 @@ ${indentedCode} }`; } } else { - // Has structure, but may still need using directives - if (!code.includes("using GitHub.Copilot;")) { - code = "using GitHub.Copilot;\n" + code; - } - if (!code.includes("using GitHub.Copilot.Rpc;")) { - code = "using GitHub.Copilot.Rpc;\n" + code; + // Has structure. Only add SDK usings if neither namespace is present; + // if the snippet declares its own using GitHub.Copilot statement, + // assume the author curated imports (avoids ambiguities like + // ModelCapabilities living in both namespaces). + if (!code.includes("using GitHub.Copilot")) { + code = "using GitHub.Copilot;\nusing GitHub.Copilot.Rpc;\n" + code; } } } From 91d89cbf27024dd16b8623c1338a9931eec3fef0 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 May 2026 17:28:22 +0100 Subject: [PATCH 14/14] Address PR review comments - go/session.go: guard against nil PermissionDecision from handler - go/README.md: fix non-compiling permission handler example - python/README.md: fix PermissionDecision* import path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- go/README.md | 7 ++++--- go/session.go | 9 +++++++++ python/README.md | 4 ++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/go/README.md b/go/README.md index ca030c007..1fca53de4 100644 --- a/go/README.md +++ b/go/README.md @@ -595,6 +595,8 @@ Provide your own `PermissionHandlerFunc` to inspect each request and apply custo ```go import ( + "fmt" + copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/rpc" ) @@ -605,9 +607,8 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi // Type-switch on the discriminated PermissionRequest variants to // access per-kind fields: if shell, ok := request.(*copilot.PermissionRequestShell); ok { - return &rpc.PermissionDecisionReject{ - Feedback: pointer(fmt.Sprintf("Refusing shell: %s", shell.FullCommandText)), - }, nil + feedback := fmt.Sprintf("Refusing shell: %s", shell.FullCommandText) + return &rpc.PermissionDecisionReject{Feedback: &feedback}, nil } return &rpc.PermissionDecisionApproveOnce{}, nil }, diff --git a/go/session.go b/go/session.go index da1324658..f38b4be17 100644 --- a/go/session.go +++ b/go/session.go @@ -1151,6 +1151,15 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques }) return } + if decision == nil { + // Handler returned (nil, nil); treat as user-not-available rather + // than sending null on the wire. + s.RPC.Permissions.HandlePendingPermissionRequest(context.Background(), &rpc.PermissionDecisionRequest{ + RequestID: requestID, + Result: &rpc.PermissionDecisionUserNotAvailable{}, + }) + return + } if _, ok := decision.(*rpc.PermissionDecisionNoResult); ok { return } diff --git a/python/README.md b/python/README.md index 38cbf40f3..3a504f966 100644 --- a/python/README.md +++ b/python/README.md @@ -575,11 +575,11 @@ Provide your own function to inspect each request and apply custom logic (sync o ```python from copilot import PermissionRequest, PermissionRequestResult -from copilot.generated.session_events import ( +from copilot.generated.rpc import ( PermissionDecisionApproveOnce, PermissionDecisionReject, - PermissionRequestShell, ) +from copilot.generated.session_events import PermissionRequestShell def on_permission_request(