Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ public CopilotClient(CopilotClientOptions? options = null)
throw new ArgumentException("CliUrl is mutually exclusive with UseStdio and CliPath");
}

// Validate auth options with external server
if (!string.IsNullOrEmpty(_options.CliUrl) && (!string.IsNullOrEmpty(_options.GithubToken) || _options.UseLoggedInUser != null))
{
throw new ArgumentException("GithubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)");
}

_logger = _options.Logger ?? NullLogger.Instance;

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

// Add auth-related flags
if (!string.IsNullOrEmpty(options.GithubToken))
{
args.AddRange(["--auth-token-env", "COPILOT_SDK_AUTH_TOKEN"]);
}

// Default UseLoggedInUser to false when GithubToken is provided
var useLoggedInUser = options.UseLoggedInUser ?? string.IsNullOrEmpty(options.GithubToken);
if (!useLoggedInUser)
{
args.Add("--no-auto-login");
}

var (fileName, processArgs) = ResolveCliCommand(cliPath, args);

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

startInfo.Environment.Remove("NODE_DEBUG");

// Set auth token in environment if provided
if (!string.IsNullOrEmpty(options.GithubToken))
{
startInfo.Environment["COPILOT_SDK_AUTH_TOKEN"] = options.GithubToken;
}

var cliProcess = new Process { StartInfo = startInfo };
cliProcess.Start();

Expand Down
15 changes: 15 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ public class CopilotClientOptions
public bool AutoRestart { get; set; } = true;
public IReadOnlyDictionary<string, string>? Environment { get; set; }
public ILogger? Logger { get; set; }

/// <summary>
/// GitHub token to use for authentication.
/// When provided, the token is passed to the CLI server via environment variable.
/// This takes priority over other authentication methods.
/// </summary>
public string? GithubToken { get; set; }

/// <summary>
/// Whether to use the logged-in user for authentication.
/// When true, the CLI server will attempt to use stored OAuth tokens or gh CLI auth.
/// When false, only explicit tokens (GithubToken or environment variables) are used.
/// Default: true (but defaults to false when GithubToken is provided).
/// </summary>
public bool? UseLoggedInUser { get; set; }
}

public class ToolBinaryResult
Expand Down
71 changes: 71 additions & 0 deletions dotnet/test/ClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,75 @@ public async Task Should_List_Models_When_Authenticated()
await client.ForceStopAsync();
}
}

[Fact]
public void Should_Accept_GithubToken_Option()
{
var options = new CopilotClientOptions
{
CliPath = _cliPath,
GithubToken = "gho_test_token"
};

Assert.Equal("gho_test_token", options.GithubToken);
}

[Fact]
public void Should_Default_UseLoggedInUser_To_Null()
{
var options = new CopilotClientOptions { CliPath = _cliPath };

Assert.Null(options.UseLoggedInUser);
}

[Fact]
public void Should_Allow_Explicit_UseLoggedInUser_False()
{
var options = new CopilotClientOptions
{
CliPath = _cliPath,
UseLoggedInUser = false
};

Assert.False(options.UseLoggedInUser);
}

[Fact]
public void Should_Allow_Explicit_UseLoggedInUser_True_With_GithubToken()
{
var options = new CopilotClientOptions
{
CliPath = _cliPath,
GithubToken = "gho_test_token",
UseLoggedInUser = true
};

Assert.True(options.UseLoggedInUser);
}

[Fact]
public void Should_Throw_When_GithubToken_Used_With_CliUrl()
{
Assert.Throws<ArgumentException>(() =>
{
_ = new CopilotClient(new CopilotClientOptions
{
CliUrl = "localhost:8080",
GithubToken = "gho_test_token"
});
});
}

[Fact]
public void Should_Throw_When_UseLoggedInUser_Used_With_CliUrl()
{
Assert.Throws<ArgumentException>(() =>
{
_ = new CopilotClient(new CopilotClientOptions
{
CliUrl = "localhost:8080",
UseLoggedInUser = false
});
});
}
}
33 changes: 32 additions & 1 deletion go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ func NewClient(options *ClientOptions) *Client {
panic("CLIUrl is mutually exclusive with UseStdio and CLIPath")
}

// Validate auth options with external server
if options.CLIUrl != "" && (options.GithubToken != "" || options.UseLoggedInUser != nil) {
panic("GithubToken and UseLoggedInUser cannot be used with CLIUrl (external server manages its own auth)")
}

// Parse CLIUrl if provided
if options.CLIUrl != "" {
host, port := parseCliUrl(options.CLIUrl)
Expand Down Expand Up @@ -148,6 +153,12 @@ func NewClient(options *ClientOptions) *Client {
if options.AutoRestart != nil {
client.autoRestart = *options.AutoRestart
}
if options.GithubToken != "" {
opts.GithubToken = options.GithubToken
}
if options.UseLoggedInUser != nil {
opts.UseLoggedInUser = options.UseLoggedInUser
}
}

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

// Add auth-related flags
if c.options.GithubToken != "" {
args = append(args, "--auth-token-env", "COPILOT_SDK_AUTH_TOKEN")
}
// Default useLoggedInUser to false when GithubToken is provided
useLoggedInUser := true
if c.options.UseLoggedInUser != nil {
useLoggedInUser = *c.options.UseLoggedInUser
} else if c.options.GithubToken != "" {
useLoggedInUser = false
}
if !useLoggedInUser {
args = append(args, "--no-auto-login")
}

// If CLIPath is a .js file, run it with node
// Note we can't rely on the shebang as Windows doesn't support it
command := c.options.CLIPath
Expand All @@ -1010,9 +1036,14 @@ func (c *Client) startCLIServer() error {
c.process.Dir = c.options.Cwd
}

// Set environment if specified
// Set environment if specified, adding auth token if needed
if len(c.options.Env) > 0 {
c.process.Env = c.options.Env
} else {
c.process.Env = os.Environ()
}
if c.options.GithubToken != "" {
c.process.Env = append(c.process.Env, "COPILOT_SDK_AUTH_TOKEN="+c.options.GithubToken)
}

if c.options.UseStdio {
Expand Down
77 changes: 77 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,83 @@ func TestClient_URLParsing(t *testing.T) {
})
}

func TestClient_AuthOptions(t *testing.T) {
t.Run("should accept GithubToken option", func(t *testing.T) {
client := NewClient(&ClientOptions{
GithubToken: "gho_test_token",
})

if client.options.GithubToken != "gho_test_token" {
t.Errorf("Expected GithubToken to be 'gho_test_token', got %q", client.options.GithubToken)
}
})

t.Run("should default UseLoggedInUser to nil when no GithubToken", func(t *testing.T) {
client := NewClient(&ClientOptions{})

if client.options.UseLoggedInUser != nil {
t.Errorf("Expected UseLoggedInUser to be nil, got %v", client.options.UseLoggedInUser)
}
})

t.Run("should allow explicit UseLoggedInUser false", func(t *testing.T) {
client := NewClient(&ClientOptions{
UseLoggedInUser: Bool(false),
})

if client.options.UseLoggedInUser == nil || *client.options.UseLoggedInUser != false {
t.Error("Expected UseLoggedInUser to be false")
}
})

t.Run("should allow explicit UseLoggedInUser true with GithubToken", func(t *testing.T) {
client := NewClient(&ClientOptions{
GithubToken: "gho_test_token",
UseLoggedInUser: Bool(true),
})

if client.options.UseLoggedInUser == nil || *client.options.UseLoggedInUser != true {
t.Error("Expected UseLoggedInUser to be true")
}
})

t.Run("should throw error when GithubToken is used with CLIUrl", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("Expected panic for auth options with CLIUrl")
} else {
matched, _ := regexp.MatchString("GithubToken and UseLoggedInUser cannot be used with CLIUrl", r.(string))
if !matched {
t.Errorf("Expected panic message about auth options, got: %v", r)
}
}
}()

NewClient(&ClientOptions{
CLIUrl: "localhost:8080",
GithubToken: "gho_test_token",
})
})

t.Run("should throw error when UseLoggedInUser is used with CLIUrl", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("Expected panic for auth options with CLIUrl")
} else {
matched, _ := regexp.MatchString("GithubToken and UseLoggedInUser cannot be used with CLIUrl", r.(string))
if !matched {
t.Errorf("Expected panic message about auth options, got: %v", r)
}
}
}()

NewClient(&ClientOptions{
CLIUrl: "localhost:8080",
UseLoggedInUser: Bool(false),
})
})
}

func findCLIPathForTest() string {
abs, _ := filepath.Abs("../nodejs/node_modules/@github/copilot/index.js")
if fileExistsForTest(abs) {
Expand Down
10 changes: 10 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ type ClientOptions struct {
AutoRestart *bool
// Env is the environment variables for the CLI process (default: inherits from current process)
Env []string
// GithubToken is the GitHub token to use for authentication.
// When provided, the token is passed to the CLI server via environment variable.
// This takes priority over other authentication methods.
GithubToken string
// UseLoggedInUser controls whether to use the logged-in user for authentication.
// When true, the CLI server will attempt to use stored OAuth tokens or gh CLI auth.
// When false, only explicit tokens (GithubToken or environment variables) are used.
// Default: true (but defaults to false when GithubToken is provided).
// Use Bool(false) to explicitly disable.
UseLoggedInUser *bool
}

// Bool returns a pointer to the given bool value.
Expand Down
31 changes: 30 additions & 1 deletion nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,13 @@ export class CopilotClient {
private actualHost: string = "localhost";
private state: ConnectionState = "disconnected";
private sessions: Map<string, CopilotSession> = new Map();
private options: Required<Omit<CopilotClientOptions, "cliUrl">> & { cliUrl?: string };
private options: Required<
Omit<CopilotClientOptions, "cliUrl" | "githubToken" | "useLoggedInUser">
> & {
cliUrl?: string;
githubToken?: string;
useLoggedInUser?: boolean;
};
private isExternalServer: boolean = false;
private forceStopping: boolean = false;

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

// Validate auth options with external server
if (options.cliUrl && (options.githubToken || options.useLoggedInUser !== undefined)) {
throw new Error(
"githubToken and useLoggedInUser cannot be used with cliUrl (external server manages its own auth)"
);
}

// Parse cliUrl if provided
if (options.cliUrl) {
const { host, port } = this.parseCliUrl(options.cliUrl);
Expand All @@ -153,6 +166,9 @@ export class CopilotClient {
autoStart: options.autoStart ?? true,
autoRestart: options.autoRestart ?? true,
env: options.env ?? process.env,
githubToken: options.githubToken,
// Default useLoggedInUser to false when githubToken is provided, otherwise true
useLoggedInUser: options.useLoggedInUser ?? (options.githubToken ? false : true),
};
}

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

// Add auth-related flags
if (this.options.githubToken) {
args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
}
if (!this.options.useLoggedInUser) {
args.push("--no-auto-login");
}

// Suppress debug/trace output that might pollute stdout
const envWithoutNodeDebug = { ...this.options.env };
delete envWithoutNodeDebug.NODE_DEBUG;

// Set auth token in environment if provided
if (this.options.githubToken) {
envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.githubToken;
}

// If cliPath is a .js file, spawn it with node
// Note that we can't rely on the shebang as Windows doesn't support it
const isJsFile = this.options.cliPath.endsWith(".js");
Expand Down
15 changes: 15 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ export interface CopilotClientOptions {
* Environment variables to pass to the CLI process. If not set, inherits process.env.
*/
env?: Record<string, string | undefined>;

/**
* GitHub token to use for authentication.
* When provided, the token is passed to the CLI server via environment variable.
* This takes priority over other authentication methods.
*/
githubToken?: string;

/**
* Whether to use the logged-in user for authentication.
* When true, the CLI server will attempt to use stored OAuth tokens or gh CLI auth.
* When false, only explicit tokens (githubToken or environment variables) are used.
* @default true (but defaults to false when githubToken is provided)
*/
useLoggedInUser?: boolean;
}

/**
Expand Down
Loading
Loading