diff --git a/docs/features/custom-agents.md b/docs/features/custom-agents.md index 716e80583..5329f163e 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 }, }) ``` @@ -173,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 @@ -198,7 +200,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 +521,7 @@ import ( "context" "fmt" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -528,8 +531,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..bd55797dd 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 }, }) ``` @@ -146,6 +147,7 @@ session, err := client.CreateSession(ctx, &copilot.SessionConfig{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class HooksExample { @@ -169,7 +171,7 @@ public static class HooksExample OnPostToolUse = onPostToolUse, }, OnPermissionRequest = (req, inv) => - Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), + Task.FromResult(PermissionDecision.ApproveOnce()), }); } } @@ -189,7 +191,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 +295,7 @@ import ( "context" "fmt" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -313,8 +316,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 @@ -348,6 +351,7 @@ session, _ := client.CreateSession(ctx, &copilot.SessionConfig{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class PermissionControlExample { @@ -376,7 +380,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..cf2dee518 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 }, }) @@ -161,6 +162,7 @@ session.Send(ctx, copilot.MessageOptions{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class ImageInputExample { @@ -171,7 +173,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 @@ -193,13 +195,14 @@ 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 { 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 +324,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -330,8 +334,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 }, }) @@ -375,6 +379,7 @@ session.Send(ctx, copilot.MessageOptions{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class BlobAttachmentExample { @@ -385,7 +390,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..3bfff10d0 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 }, }) @@ -202,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 { @@ -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..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() { @@ -91,8 +92,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 { @@ -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 @@ -127,7 +129,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 +213,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -220,8 +223,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 @@ -244,6 +247,7 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class SkillsExample { @@ -256,7 +260,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..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() { @@ -118,8 +119,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 { @@ -152,13 +153,14 @@ 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 { 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 +303,7 @@ package main import ( "context" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -310,8 +313,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 }, }) @@ -360,6 +363,7 @@ session.Send(ctx, copilot.MessageOptions{ ```csharp using GitHub.Copilot; +using GitHub.Copilot.Rpc; public static class QueueingExample { @@ -370,7 +374,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/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/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..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 NoResultPermissionV2ErrorMessage = - "Permission handlers cannot return 'no-result' when connected to a protocol v2 server."; - /// /// 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,111 +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 result = await session.HandlePermissionRequestAsync(permissionRequest); - if (result.Kind == new PermissionRequestResultKind("no-result")) - { - throw new InvalidOperationException(NoResultPermissionV2ErrorMessage); - } - return new PermissionRequestResponseV2(result); - } - catch (InvalidOperationException ex) when (ex.Message == NoResultPermissionV2ErrorMessage) - { - throw; - } - catch (Exception) - { - return new PermissionRequestResponseV2(new PermissionRequestResult - { - Kind = PermissionRequestResultKind.UserNotAvailable - }); - } - } } private class Connection( @@ -2074,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( - PermissionRequestResult Result); - [JsonSourceGenerationOptions( JsonSerializerDefaults.Web, AllowOutOfOrderMetadataProperties = true, @@ -2103,9 +1982,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))] [JsonSerializable(typeof(ResumeSessionResponse))] @@ -2116,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/dotnet/src/PermissionDecision.cs b/dotnet/src/PermissionDecision.cs new file mode 100644 index 000000000..54e123791 --- /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 PermissionDecision ApproveOnce() => new PermissionDecisionApproveOnce(); + + /// Reject the request, optionally forwarding feedback to the LLM. + public static PermissionDecision Reject(string? feedback = null) => + new PermissionDecisionReject { Feedback = feedback }; + + /// Deny the request because no user is available to confirm it. + public static PermissionDecision UserNotAvailable() => new PermissionDecisionUserNotAvailable(); + + /// + /// Decline to respond to this permission request, allowing another + /// connected client to answer instead. + /// + public static PermissionDecision NoResult() => new PermissionDecisionNoResult(); +} 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..1fca53de4 100644 --- a/go/README.md +++ b/go/README.md @@ -594,45 +594,39 @@ session, err := client.CreateSession(context.Background(), &copilot.SessionConfi Provide your own `PermissionHandlerFunc` to inspect each request and apply custom logic: ```go +import ( + "fmt" + + 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 { + feedback := fmt.Sprintf("Refusing shell: %s", shell.FullCommandText) + return &rpc.PermissionDecisionReject{Feedback: &feedback}, 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..9491eb199 100644 --- a/go/client.go +++ b/go/client.go @@ -52,8 +52,6 @@ import ( "github.com/github/copilot-sdk/go/rpc" ) -const noResultPermissionV2Error = "permission handlers cannot return 'no-result' when connected to a protocol v2 server" - 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,120 +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 PermissionRequestResult `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: PermissionRequestResult{ - Kind: PermissionRequestResultKindDeniedCouldNotRequestFromUser, - }, - }, nil - } - - invocation := PermissionInvocation{ - SessionID: session.SessionID, - } - - result, err := handler(req.Request, invocation) - if err != nil { - return &permissionResponseV2{ - Result: PermissionRequestResult{ - Kind: PermissionRequestResultKindDeniedCouldNotRequestFromUser, - }, - }, nil - } - if result.Kind == "no-result" { - return nil, &jsonrpc2.Error{Code: -32603, Message: noResultPermissionV2Error} - } - - return &permissionResponseV2{Result: result}, nil -} diff --git a/go/internal/e2e/multi_client_e2e_test.go b/go/internal/e2e/multi_client_e2e_test.go index 9dd8a15bf..a5c852bc8 100644 --- a/go/internal/e2e/multi_client_e2e_test.go +++ b/go/internal/e2e/multi_client_e2e_test.go @@ -11,6 +11,7 @@ import ( copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" ) func TestMultiClientE2E(t *testing.T) { @@ -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..672481a4f 100644 --- a/go/internal/e2e/suspend_e2e_test.go +++ b/go/internal/e2e/suspend_e2e_test.go @@ -9,6 +9,7 @@ import ( copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" ) const suspendTimeout = 60 * time.Second @@ -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..621f7758d 100644 --- a/go/internal/e2e/tools_e2e_test.go +++ b/go/internal/e2e/tools_e2e_test.go @@ -10,6 +10,7 @@ import ( copilot "github.com/github/copilot-sdk/go" "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" ) func TestToolsE2E(t *testing.T) { @@ -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..f38b4be17 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,28 @@ func (s *Session) executePermissionAndRespond(requestID string, permissionReques }) return } - if result.Kind == "no-result" { + 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 + } + 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/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..21563d598 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -32,13 +32,12 @@ 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 { AutoModeSwitchRequest, AutoModeSwitchResponse, - ConnectionState, CopilotClientOptions, CustomAgentConfig, ExitPlanModeRequest, @@ -62,9 +61,6 @@ import type { SystemMessageCustomizeConfig, TelemetryConfig, Tool, - ToolCallRequestPayload, - ToolCallResponsePayload, - ToolResultObject, TraceContextProvider, TypedSessionLifecycleHandler, } from "./types.js"; @@ -74,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) @@ -251,7 +247,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 +1035,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. * @@ -1856,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: { @@ -2122,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/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/session.ts b/nodejs/src/session.ts index 4baf35c3e..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, @@ -50,10 +49,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 +902,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/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..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(); @@ -984,7 +970,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 +978,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..cd67cf672 100644 --- a/nodejs/test/e2e/client_options.e2e.test.ts +++ b/nodejs/test/e2e/client_options.e2e.test.ts @@ -154,10 +154,7 @@ 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 +180,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"); diff --git a/python/README.md b/python/README.md index 4e415e320..3a504f966 100644 --- a/python/README.md +++ b/python/README.md @@ -574,13 +574,12 @@ 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.rpc import ( PermissionDecisionApproveOnce, PermissionDecisionReject, - PermissionRequest, - PermissionRequestResult, - PermissionRequestShell, ) +from copilot.generated.session_events import PermissionRequestShell def on_permission_request( diff --git a/python/copilot/client.py b/python/copilot/client.py index 7e381dce9..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__) @@ -159,9 +156,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 @@ -938,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: @@ -2983,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 @@ -3108,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 @@ -3253,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/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 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/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/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/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..b07133eb7 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,16 @@ 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 sent back to the +/// server via `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), - })), - } -} - -/// Map a permission result into 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. -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::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 => None, + PermissionResult::Decision(decision) => Some( + serde_json::to_value(decision).expect("serializing permission decision should succeed"), + ), } } @@ -1948,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 = @@ -2139,108 +2027,31 @@ 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::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 - ); - - // Deferred → falls back to Approved because the direct RPC must reply. - assert_eq!( - direct_permission_payload(&PermissionResult::Deferred), - json!({ "kind": "approve-once" }) - ); - - // NoResult -> direct RPC cannot be left pending, so report no user. - assert_eq!( - direct_permission_payload(&PermissionResult::NoResult), - json!({ "kind": "user-not-available" }) - ); - - // Approved/Denied → existing kind-only shape. assert_eq!( - direct_permission_payload(&PermissionResult::Approved), - json!({ "kind": "approve-once" }) + notification_permission_payload(&PermissionResult::reject(Some("bad".to_string()))), + Some(json!({ "kind": "reject", "feedback": "bad" })) ); assert_eq!( - direct_permission_payload(&PermissionResult::Denied), - json!({ "kind": "reject" }) + notification_permission_payload(&PermissionResult::user_not_available()), + Some(json!({ "kind": "user-not-available" })) ); } } 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..5140d5413 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -8,13 +8,12 @@ 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, - 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; @@ -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::Denied - } - } - - 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] diff --git a/scripts/docs-validation/extract.ts b/scripts/docs-validation/extract.ts index 0b0879db1..b8d7cb089 100644 --- a/scripts/docs-validation/extract.ts +++ b/scripts/docs-validation/extract.ts @@ -301,9 +301,16 @@ 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 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;"); + usings.push("using GitHub.Copilot.Rpc;"); } // Generate a unique class name based on block location @@ -335,9 +342,12 @@ ${indentedCode} }`; } } else { - // Has structure, but may still need using directive - if (!code.includes("using GitHub.Copilot;")) { - code = "using GitHub.Copilot;\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; } } } 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 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 + + 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 ae1ea92a0..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 { @@ -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-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 eece7a9cd..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 { @@ -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-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 8232c63dc..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 { @@ -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-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 01d0b6da9..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 { @@ -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/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/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/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/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..51b592431 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(nil) 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..7df9cd888 100644 --- a/test/scenarios/bundling/fully-bundled/typescript/src/index.ts +++ b/test/scenarios/bundling/fully-bundled/typescript/src/index.ts @@ -1,9 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, + 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 78184df2a..b20addf39 100644 --- a/test/scenarios/callbacks/hooks/csharp/Program.cs +++ b/test/scenarios/callbacks/hooks/csharp/Program.cs @@ -1,12 +1,9 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; var hookLog = new List(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); @@ -16,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 4ef48b483..d080a6ae1 100644 --- a/test/scenarios/callbacks/hooks/go/main.go +++ b/test/scenarios/callbacks/hooks/go/main.go @@ -4,10 +4,10 @@ import ( "context" "fmt" "log" - "os" "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -22,9 +22,7 @@ func main() { hookLogMu.Unlock() } - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -34,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/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..c712994a9 100644 --- a/test/scenarios/callbacks/hooks/typescript/src/index.ts +++ b/test/scenarios/callbacks/hooks/typescript/src/index.ts @@ -1,12 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -37,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 cf3275e56..7818580fb 100644 --- a/test/scenarios/callbacks/permissions/csharp/Program.cs +++ b/test/scenarios/callbacks/permissions/csharp/Program.cs @@ -1,12 +1,9 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; var permissionLog = new List(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); @@ -27,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 23715727b..428ab0a70 100644 --- a/test/scenarios/callbacks/permissions/go/main.go +++ b/test/scenarios/callbacks/permissions/go/main.go @@ -4,10 +4,10 @@ import ( "context" "fmt" "log" - "os" "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { @@ -16,9 +16,7 @@ func main() { permissionLogMu sync.Mutex ) - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -28,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) { @@ -41,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/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..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() } } @@ -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..861f5a654 100644 --- a/test/scenarios/callbacks/permissions/typescript/src/index.ts +++ b/test/scenarios/callbacks/permissions/typescript/src/index.ts @@ -1,12 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + 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 e9fe06968..e6988c6cb 100644 --- a/test/scenarios/callbacks/user-input/csharp/Program.cs +++ b/test/scenarios/callbacks/user-input/csharp/Program.cs @@ -1,12 +1,9 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; var inputLog = new List(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); @@ -16,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 a0baf2936..673667058 100644 --- a/test/scenarios/callbacks/user-input/go/main.go +++ b/test/scenarios/callbacks/user-input/go/main.go @@ -4,10 +4,10 @@ import ( "context" "fmt" "log" - "os" "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) var ( @@ -16,9 +16,7 @@ var ( ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -28,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/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..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() } } @@ -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..e7e3d0bca 100644 --- a/test/scenarios/callbacks/user-input/typescript/src/index.ts +++ b/test/scenarios/callbacks/user-input/typescript/src/index.ts @@ -1,12 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -22,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 23d6c63f0..71ea173aa 100644 --- a/test/scenarios/modes/default/csharp/Program.cs +++ b/test/scenarios/modes/default/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 b0c44459f..7cefdcf26 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(nil) 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..c6db7562d 100644 --- a/test/scenarios/modes/default/typescript/src/index.ts +++ b/test/scenarios/modes/default/typescript/src/index.ts @@ -1,10 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -12,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 70081b58d..166048d34 100644 --- a/test/scenarios/modes/minimal/csharp/Program.cs +++ b/test/scenarios/modes/minimal/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 dc9ad0190..4cb891885 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(nil) 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..68fa73752 100644 --- a/test/scenarios/modes/minimal/typescript/src/index.ts +++ b/test/scenarios/modes/minimal/typescript/src/index.ts @@ -1,10 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + 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 7cafcb86d..8983af2d9 100644 --- a/test/scenarios/prompts/attachments/csharp/Program.cs +++ b/test/scenarios/prompts/attachments/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 44c79cf6c..4acfee001 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(nil) 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..3de4b757a 100644 --- a/test/scenarios/prompts/attachments/typescript/src/index.ts +++ b/test/scenarios/prompts/attachments/typescript/src/index.ts @@ -1,14 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -16,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 2ed2ae94d..36dc52e44 100644 --- a/test/scenarios/prompts/reasoning-effort/csharp/Program.cs +++ b/test/scenarios/prompts/reasoning-effort/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 af5381263..ca3af77d3 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(nil) 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..da937738d 100644 --- a/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts +++ b/test/scenarios/prompts/reasoning-effort/typescript/src/index.ts @@ -1,10 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + 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 48afdd7ba..cebd2417c 100644 --- a/test/scenarios/prompts/system-message/csharp/Program.cs +++ b/test/scenarios/prompts/system-message/csharp/Program.cs @@ -2,11 +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")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 a49d65d88..d0b1aee5e 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(nil) 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..27525a28f 100644 --- a/test/scenarios/prompts/system-message/typescript/src/index.ts +++ b/test/scenarios/prompts/system-message/typescript/src/index.ts @@ -1,12 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + 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 a5ba2577b..d53dc41f1 100644 --- a/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs +++ b/test/scenarios/sessions/concurrent-sessions/csharp/Program.cs @@ -3,11 +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")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 e399fedf7..342d0f6cd 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(nil) 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..196249e82 100644 --- a/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/concurrent-sessions/typescript/src/index.ts @@ -1,13 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + 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 2619f25b4..f5a2ff183 100644 --- a/test/scenarios/sessions/infinite-sessions/csharp/Program.cs +++ b/test/scenarios/sessions/infinite-sessions/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 29871eacc..62d640850 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(nil) 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..f10543ab0 100644 --- a/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts +++ b/test/scenarios/sessions/infinite-sessions/typescript/src/index.ts @@ -1,10 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -12,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, }, }); @@ -35,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 a1bd015bd..4f8bbdb5a 100644 --- a/test/scenarios/sessions/session-resume/csharp/Program.cs +++ b/test/scenarios/sessions/session-resume/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 330fb6852..365c58ec6 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(nil) 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..125b89341 100644 --- a/test/scenarios/sessions/session-resume/typescript/src/index.ts +++ b/test/scenarios/sessions/session-resume/typescript/src/index.ts @@ -1,10 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + 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 518d4b227..706960901 100644 --- a/test/scenarios/sessions/streaming/csharp/Program.cs +++ b/test/scenarios/sessions/streaming/csharp/Program.cs @@ -1,17 +1,6 @@ using GitHub.Copilot; -var options = new CopilotClientOptions -{ - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}; - -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 8a1c78efa..e2a11029a 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(nil) 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..25df8cb4b 100644 --- a/test/scenarios/sessions/streaming/typescript/src/index.ts +++ b/test/scenarios/sessions/streaming/typescript/src/index.ts @@ -1,10 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + 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 6c5b980cc..f2db5fe5b 100644 --- a/test/scenarios/tools/custom-agents/csharp/Program.cs +++ b/test/scenarios/tools/custom-agents/csharp/Program.cs @@ -1,13 +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), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 1e6ada739..e52404a6a 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(nil) 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..4a48902f8 100644 --- a/test/scenarios/tools/custom-agents/typescript/src/index.ts +++ b/test/scenarios/tools/custom-agents/typescript/src/index.ts @@ -1,19 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -26,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 ed667825f..7d7fe4738 100644 --- a/test/scenarios/tools/mcp-servers/csharp/Program.cs +++ b/test/scenarios/tools/mcp-servers/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 b1a1225f1..dd6ec5219 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(nil) 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..838094c8d 100644 --- a/test/scenarios/tools/mcp-servers/typescript/src/index.ts +++ b/test/scenarios/tools/mcp-servers/typescript/src/index.ts @@ -1,10 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { // MCP server config — demonstrates the configuration pattern. @@ -15,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(" ") + : [], }; } @@ -38,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 a0ea0eefe..a9a2e0308 100644 --- a/test/scenarios/tools/no-tools/csharp/Program.cs +++ b/test/scenarios/tools/no-tools/csharp/Program.cs @@ -7,11 +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")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 5d1aa872f..9698ebded 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(nil) 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..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,10 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + 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 81adf96a5..5e3f3c859 100644 --- a/test/scenarios/tools/skills/csharp/Program.cs +++ b/test/scenarios/tools/skills/csharp/Program.cs @@ -1,10 +1,7 @@ using GitHub.Copilot; +using GitHub.Copilot.Rpc; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); @@ -17,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 7b0ef8032..21d9604f7 100644 --- a/test/scenarios/tools/skills/go/main.go +++ b/test/scenarios/tools/skills/go/main.go @@ -4,17 +4,15 @@ import ( "context" "fmt" "log" - "os" "path/filepath" "runtime" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) func main() { - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -28,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/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..0a934eb39 100644 --- a/test/scenarios/tools/skills/typescript/src/index.ts +++ b/test/scenarios/tools/skills/typescript/src/index.ts @@ -1,14 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + 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 72431c005..23f6bf4b4 100644 --- a/test/scenarios/tools/tool-filtering/csharp/Program.cs +++ b/test/scenarios/tools/tool-filtering/csharp/Program.cs @@ -1,10 +1,6 @@ using GitHub.Copilot; -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 e4a958be2..646582bdf 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(nil) 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..7ab2d2f93 100644 --- a/test/scenarios/tools/tool-filtering/typescript/src/index.ts +++ b/test/scenarios/tools/tool-filtering/typescript/src/index.ts @@ -1,17 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + 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 a8c7679de..c88b8dc2c 100644 --- a/test/scenarios/tools/tool-overrides/csharp/Program.cs +++ b/test/scenarios/tools/tool-overrides/csharp/Program.cs @@ -2,11 +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")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +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 8d5f6a756..9f77fc56d 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(nil) 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..fa3fc457b 100644 --- a/test/scenarios/tools/tool-overrides/typescript/src/index.ts +++ b/test/scenarios/tools/tool-overrides/typescript/src/index.ts @@ -1,11 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ @@ -13,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 93ad41f1e..64704ff3f 100644 --- a/test/scenarios/tools/virtual-filesystem/csharp/Program.cs +++ b/test/scenarios/tools/virtual-filesystem/csharp/Program.cs @@ -1,15 +1,12 @@ using System.ComponentModel; using GitHub.Copilot; +using GitHub.Copilot.Rpc; using Microsoft.Extensions.AI; // In-memory virtual filesystem var virtualFs = new Dictionary(); -using var client = new CopilotClient(new CopilotClientOptions -{ - Connection = RuntimeConnection.ForStdio(path: Environment.GetEnvironmentVariable("COPILOT_CLI_PATH")), - GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"), -}); +using var client = new CopilotClient(); await client.StartAsync(); @@ -49,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 de4b50637..84dccf7f4 100644 --- a/test/scenarios/tools/virtual-filesystem/go/main.go +++ b/test/scenarios/tools/virtual-filesystem/go/main.go @@ -4,11 +4,11 @@ import ( "context" "fmt" "log" - "os" "strings" "sync" copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/rpc" ) // In-memory virtual filesystem @@ -73,9 +73,7 @@ func main() { }, } - client := copilot.NewClient(&copilot.ClientOptions{ - GitHubToken: os.Getenv("GITHUB_TOKEN"), - }) + client := copilot.NewClient(nil) ctx := context.Background() if err := client.Start(ctx); err != nil { @@ -88,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) { 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..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,10 +39,7 @@ 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, - }); + const client = new CopilotClient(); try { const session = await client.createSession({ 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/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/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..51b592431 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(nil) 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..7df9cd888 100644 --- a/test/scenarios/transport/stdio/typescript/src/index.ts +++ b/test/scenarios/transport/stdio/typescript/src/index.ts @@ -1,9 +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 }), - gitHubToken: process.env.GITHUB_TOKEN, + connection: RuntimeConnection.forStdio({ + path: process.env.COPILOT_CLI_PATH, + }), }); 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(); 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 {