Skip to content

Commit 2fe7352

Browse files
Add githubToken and useLoggedInUser options to all SDK clients (#237)
* feat(auth): add githubToken and useLoggedInUser options to all SDK clients Enable SDK clients to customize authentication when spawning the CLI server. Node.js: - Add githubToken and useLoggedInUser options to CopilotClientOptions - Set COPILOT_SDK_AUTH_TOKEN env var and pass --auth-token-env flag - Pass --no-auto-login when useLoggedInUser is false - Default useLoggedInUser to false when githubToken is provided Python: - Add github_token and use_logged_in_user options - Same behavior as Node.js SDK Go: - Add GithubToken and UseLoggedInUser fields to ClientOptions - Same behavior as Node.js SDK .NET: - Add GithubToken and UseLoggedInUser properties to CopilotClientOptions - Same behavior as Node.js SDK All SDKs include validation to prevent use with cliUrl (external server) and tests for the new options. * Potential fix for pull request finding 'Unnecessarily complex Boolean expression' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
1 parent fc9b54e commit 2fe7352

File tree

12 files changed

+428
-2
lines changed

12 files changed

+428
-2
lines changed

dotnet/src/Client.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ public CopilotClient(CopilotClientOptions? options = null)
9090
throw new ArgumentException("CliUrl is mutually exclusive with UseStdio and CliPath");
9191
}
9292

93+
// Validate auth options with external server
94+
if (!string.IsNullOrEmpty(_options.CliUrl) && (!string.IsNullOrEmpty(_options.GithubToken) || _options.UseLoggedInUser != null))
95+
{
96+
throw new ArgumentException("GithubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)");
97+
}
98+
9399
_logger = _options.Logger ?? NullLogger.Instance;
94100

95101
// Parse CliUrl if provided
@@ -657,6 +663,19 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
657663
args.AddRange(["--port", options.Port.ToString()]);
658664
}
659665

666+
// Add auth-related flags
667+
if (!string.IsNullOrEmpty(options.GithubToken))
668+
{
669+
args.AddRange(["--auth-token-env", "COPILOT_SDK_AUTH_TOKEN"]);
670+
}
671+
672+
// Default UseLoggedInUser to false when GithubToken is provided
673+
var useLoggedInUser = options.UseLoggedInUser ?? string.IsNullOrEmpty(options.GithubToken);
674+
if (!useLoggedInUser)
675+
{
676+
args.Add("--no-auto-login");
677+
}
678+
660679
var (fileName, processArgs) = ResolveCliCommand(cliPath, args);
661680

662681
var startInfo = new ProcessStartInfo
@@ -682,6 +701,12 @@ private async Task VerifyProtocolVersionAsync(Connection connection, Cancellatio
682701

683702
startInfo.Environment.Remove("NODE_DEBUG");
684703

704+
// Set auth token in environment if provided
705+
if (!string.IsNullOrEmpty(options.GithubToken))
706+
{
707+
startInfo.Environment["COPILOT_SDK_AUTH_TOKEN"] = options.GithubToken;
708+
}
709+
685710
var cliProcess = new Process { StartInfo = startInfo };
686711
cliProcess.Start();
687712

dotnet/src/Types.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@ public class CopilotClientOptions
3535
public bool AutoRestart { get; set; } = true;
3636
public IReadOnlyDictionary<string, string>? Environment { get; set; }
3737
public ILogger? Logger { get; set; }
38+
39+
/// <summary>
40+
/// GitHub token to use for authentication.
41+
/// When provided, the token is passed to the CLI server via environment variable.
42+
/// This takes priority over other authentication methods.
43+
/// </summary>
44+
public string? GithubToken { get; set; }
45+
46+
/// <summary>
47+
/// Whether to use the logged-in user for authentication.
48+
/// When true, the CLI server will attempt to use stored OAuth tokens or gh CLI auth.
49+
/// When false, only explicit tokens (GithubToken or environment variables) are used.
50+
/// Default: true (but defaults to false when GithubToken is provided).
51+
/// </summary>
52+
public bool? UseLoggedInUser { get; set; }
3853
}
3954

4055
public class ToolBinaryResult

dotnet/test/ClientTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,4 +172,75 @@ public async Task Should_List_Models_When_Authenticated()
172172
await client.ForceStopAsync();
173173
}
174174
}
175+
176+
[Fact]
177+
public void Should_Accept_GithubToken_Option()
178+
{
179+
var options = new CopilotClientOptions
180+
{
181+
CliPath = _cliPath,
182+
GithubToken = "gho_test_token"
183+
};
184+
185+
Assert.Equal("gho_test_token", options.GithubToken);
186+
}
187+
188+
[Fact]
189+
public void Should_Default_UseLoggedInUser_To_Null()
190+
{
191+
var options = new CopilotClientOptions { CliPath = _cliPath };
192+
193+
Assert.Null(options.UseLoggedInUser);
194+
}
195+
196+
[Fact]
197+
public void Should_Allow_Explicit_UseLoggedInUser_False()
198+
{
199+
var options = new CopilotClientOptions
200+
{
201+
CliPath = _cliPath,
202+
UseLoggedInUser = false
203+
};
204+
205+
Assert.False(options.UseLoggedInUser);
206+
}
207+
208+
[Fact]
209+
public void Should_Allow_Explicit_UseLoggedInUser_True_With_GithubToken()
210+
{
211+
var options = new CopilotClientOptions
212+
{
213+
CliPath = _cliPath,
214+
GithubToken = "gho_test_token",
215+
UseLoggedInUser = true
216+
};
217+
218+
Assert.True(options.UseLoggedInUser);
219+
}
220+
221+
[Fact]
222+
public void Should_Throw_When_GithubToken_Used_With_CliUrl()
223+
{
224+
Assert.Throws<ArgumentException>(() =>
225+
{
226+
_ = new CopilotClient(new CopilotClientOptions
227+
{
228+
CliUrl = "localhost:8080",
229+
GithubToken = "gho_test_token"
230+
});
231+
});
232+
}
233+
234+
[Fact]
235+
public void Should_Throw_When_UseLoggedInUser_Used_With_CliUrl()
236+
{
237+
Assert.Throws<ArgumentException>(() =>
238+
{
239+
_ = new CopilotClient(new CopilotClientOptions
240+
{
241+
CliUrl = "localhost:8080",
242+
UseLoggedInUser = false
243+
});
244+
});
245+
}
175246
}

go/client.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ func NewClient(options *ClientOptions) *Client {
115115
panic("CLIUrl is mutually exclusive with UseStdio and CLIPath")
116116
}
117117

118+
// Validate auth options with external server
119+
if options.CLIUrl != "" && (options.GithubToken != "" || options.UseLoggedInUser != nil) {
120+
panic("GithubToken and UseLoggedInUser cannot be used with CLIUrl (external server manages its own auth)")
121+
}
122+
118123
// Parse CLIUrl if provided
119124
if options.CLIUrl != "" {
120125
host, port := parseCliUrl(options.CLIUrl)
@@ -148,6 +153,12 @@ func NewClient(options *ClientOptions) *Client {
148153
if options.AutoRestart != nil {
149154
client.autoRestart = *options.AutoRestart
150155
}
156+
if options.GithubToken != "" {
157+
opts.GithubToken = options.GithubToken
158+
}
159+
if options.UseLoggedInUser != nil {
160+
opts.UseLoggedInUser = options.UseLoggedInUser
161+
}
151162
}
152163

153164
// Check environment variable for CLI path
@@ -995,6 +1006,21 @@ func (c *Client) startCLIServer() error {
9951006
args = append(args, "--port", strconv.Itoa(c.options.Port))
9961007
}
9971008

1009+
// Add auth-related flags
1010+
if c.options.GithubToken != "" {
1011+
args = append(args, "--auth-token-env", "COPILOT_SDK_AUTH_TOKEN")
1012+
}
1013+
// Default useLoggedInUser to false when GithubToken is provided
1014+
useLoggedInUser := true
1015+
if c.options.UseLoggedInUser != nil {
1016+
useLoggedInUser = *c.options.UseLoggedInUser
1017+
} else if c.options.GithubToken != "" {
1018+
useLoggedInUser = false
1019+
}
1020+
if !useLoggedInUser {
1021+
args = append(args, "--no-auto-login")
1022+
}
1023+
9981024
// If CLIPath is a .js file, run it with node
9991025
// Note we can't rely on the shebang as Windows doesn't support it
10001026
command := c.options.CLIPath
@@ -1010,9 +1036,14 @@ func (c *Client) startCLIServer() error {
10101036
c.process.Dir = c.options.Cwd
10111037
}
10121038

1013-
// Set environment if specified
1039+
// Set environment if specified, adding auth token if needed
10141040
if len(c.options.Env) > 0 {
10151041
c.process.Env = c.options.Env
1042+
} else {
1043+
c.process.Env = os.Environ()
1044+
}
1045+
if c.options.GithubToken != "" {
1046+
c.process.Env = append(c.process.Env, "COPILOT_SDK_AUTH_TOKEN="+c.options.GithubToken)
10161047
}
10171048

10181049
if c.options.UseStdio {

go/client_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,83 @@ func TestClient_URLParsing(t *testing.T) {
237237
})
238238
}
239239

240+
func TestClient_AuthOptions(t *testing.T) {
241+
t.Run("should accept GithubToken option", func(t *testing.T) {
242+
client := NewClient(&ClientOptions{
243+
GithubToken: "gho_test_token",
244+
})
245+
246+
if client.options.GithubToken != "gho_test_token" {
247+
t.Errorf("Expected GithubToken to be 'gho_test_token', got %q", client.options.GithubToken)
248+
}
249+
})
250+
251+
t.Run("should default UseLoggedInUser to nil when no GithubToken", func(t *testing.T) {
252+
client := NewClient(&ClientOptions{})
253+
254+
if client.options.UseLoggedInUser != nil {
255+
t.Errorf("Expected UseLoggedInUser to be nil, got %v", client.options.UseLoggedInUser)
256+
}
257+
})
258+
259+
t.Run("should allow explicit UseLoggedInUser false", func(t *testing.T) {
260+
client := NewClient(&ClientOptions{
261+
UseLoggedInUser: Bool(false),
262+
})
263+
264+
if client.options.UseLoggedInUser == nil || *client.options.UseLoggedInUser != false {
265+
t.Error("Expected UseLoggedInUser to be false")
266+
}
267+
})
268+
269+
t.Run("should allow explicit UseLoggedInUser true with GithubToken", func(t *testing.T) {
270+
client := NewClient(&ClientOptions{
271+
GithubToken: "gho_test_token",
272+
UseLoggedInUser: Bool(true),
273+
})
274+
275+
if client.options.UseLoggedInUser == nil || *client.options.UseLoggedInUser != true {
276+
t.Error("Expected UseLoggedInUser to be true")
277+
}
278+
})
279+
280+
t.Run("should throw error when GithubToken is used with CLIUrl", func(t *testing.T) {
281+
defer func() {
282+
if r := recover(); r == nil {
283+
t.Error("Expected panic for auth options with CLIUrl")
284+
} else {
285+
matched, _ := regexp.MatchString("GithubToken and UseLoggedInUser cannot be used with CLIUrl", r.(string))
286+
if !matched {
287+
t.Errorf("Expected panic message about auth options, got: %v", r)
288+
}
289+
}
290+
}()
291+
292+
NewClient(&ClientOptions{
293+
CLIUrl: "localhost:8080",
294+
GithubToken: "gho_test_token",
295+
})
296+
})
297+
298+
t.Run("should throw error when UseLoggedInUser is used with CLIUrl", func(t *testing.T) {
299+
defer func() {
300+
if r := recover(); r == nil {
301+
t.Error("Expected panic for auth options with CLIUrl")
302+
} else {
303+
matched, _ := regexp.MatchString("GithubToken and UseLoggedInUser cannot be used with CLIUrl", r.(string))
304+
if !matched {
305+
t.Errorf("Expected panic message about auth options, got: %v", r)
306+
}
307+
}
308+
}()
309+
310+
NewClient(&ClientOptions{
311+
CLIUrl: "localhost:8080",
312+
UseLoggedInUser: Bool(false),
313+
})
314+
})
315+
}
316+
240317
func findCLIPathForTest() string {
241318
abs, _ := filepath.Abs("../nodejs/node_modules/@github/copilot/index.js")
242319
if fileExistsForTest(abs) {

go/types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ type ClientOptions struct {
3535
AutoRestart *bool
3636
// Env is the environment variables for the CLI process (default: inherits from current process)
3737
Env []string
38+
// GithubToken is the GitHub token to use for authentication.
39+
// When provided, the token is passed to the CLI server via environment variable.
40+
// This takes priority over other authentication methods.
41+
GithubToken string
42+
// UseLoggedInUser controls whether to use the logged-in user for authentication.
43+
// When true, the CLI server will attempt to use stored OAuth tokens or gh CLI auth.
44+
// When false, only explicit tokens (GithubToken or environment variables) are used.
45+
// Default: true (but defaults to false when GithubToken is provided).
46+
// Use Bool(false) to explicitly disable.
47+
UseLoggedInUser *bool
3848
}
3949

4050
// Bool returns a pointer to the given bool value.

nodejs/src/client.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,13 @@ export class CopilotClient {
103103
private actualHost: string = "localhost";
104104
private state: ConnectionState = "disconnected";
105105
private sessions: Map<string, CopilotSession> = new Map();
106-
private options: Required<Omit<CopilotClientOptions, "cliUrl">> & { cliUrl?: string };
106+
private options: Required<
107+
Omit<CopilotClientOptions, "cliUrl" | "githubToken" | "useLoggedInUser">
108+
> & {
109+
cliUrl?: string;
110+
githubToken?: string;
111+
useLoggedInUser?: boolean;
112+
};
107113
private isExternalServer: boolean = false;
108114
private forceStopping: boolean = false;
109115

@@ -134,6 +140,13 @@ export class CopilotClient {
134140
throw new Error("cliUrl is mutually exclusive with useStdio and cliPath");
135141
}
136142

143+
// Validate auth options with external server
144+
if (options.cliUrl && (options.githubToken || options.useLoggedInUser !== undefined)) {
145+
throw new Error(
146+
"githubToken and useLoggedInUser cannot be used with cliUrl (external server manages its own auth)"
147+
);
148+
}
149+
137150
// Parse cliUrl if provided
138151
if (options.cliUrl) {
139152
const { host, port } = this.parseCliUrl(options.cliUrl);
@@ -153,6 +166,9 @@ export class CopilotClient {
153166
autoStart: options.autoStart ?? true,
154167
autoRestart: options.autoRestart ?? true,
155168
env: options.env ?? process.env,
169+
githubToken: options.githubToken,
170+
// Default useLoggedInUser to false when githubToken is provided, otherwise true
171+
useLoggedInUser: options.useLoggedInUser ?? (options.githubToken ? false : true),
156172
};
157173
}
158174

@@ -758,10 +774,23 @@ export class CopilotClient {
758774
args.push("--port", this.options.port.toString());
759775
}
760776

777+
// Add auth-related flags
778+
if (this.options.githubToken) {
779+
args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
780+
}
781+
if (!this.options.useLoggedInUser) {
782+
args.push("--no-auto-login");
783+
}
784+
761785
// Suppress debug/trace output that might pollute stdout
762786
const envWithoutNodeDebug = { ...this.options.env };
763787
delete envWithoutNodeDebug.NODE_DEBUG;
764788

789+
// Set auth token in environment if provided
790+
if (this.options.githubToken) {
791+
envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.githubToken;
792+
}
793+
765794
// If cliPath is a .js file, spawn it with node
766795
// Note that we can't rely on the shebang as Windows doesn't support it
767796
const isJsFile = this.options.cliPath.endsWith(".js");

nodejs/src/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ export interface CopilotClientOptions {
7474
* Environment variables to pass to the CLI process. If not set, inherits process.env.
7575
*/
7676
env?: Record<string, string | undefined>;
77+
78+
/**
79+
* GitHub token to use for authentication.
80+
* When provided, the token is passed to the CLI server via environment variable.
81+
* This takes priority over other authentication methods.
82+
*/
83+
githubToken?: string;
84+
85+
/**
86+
* Whether to use the logged-in user for authentication.
87+
* When true, the CLI server will attempt to use stored OAuth tokens or gh CLI auth.
88+
* When false, only explicit tokens (githubToken or environment variables) are used.
89+
* @default true (but defaults to false when githubToken is provided)
90+
*/
91+
useLoggedInUser?: boolean;
7792
}
7893

7994
/**

0 commit comments

Comments
 (0)