Skip to content

Per-session GitHub authentication for all SDK languages#1124

Open
SteveSandersonMS wants to merge 16 commits intomainfrom
stevesa/auth-isolation
Open

Per-session GitHub authentication for all SDK languages#1124
SteveSandersonMS wants to merge 16 commits intomainfrom
stevesa/auth-isolation

Conversation

@SteveSandersonMS
Copy link
Copy Markdown
Contributor

Summary

Adds per-session GitHub authentication support across all SDK languages (Node, Python, Go, C#). This allows SDK clients to pass a githubToken when creating a session, so each session can authenticate as a distinct GitHub user instead of sharing the process-level auth.

Changes

Node SDK

  • Added \githubToken\ parameter to \SessionCreateParams\ and \SessionResumeParams\
  • E2E tests for per-session auth flow

Python SDK

  • Updated codegen output with \githubToken\ in session create/resume params
  • E2E tests for per-session auth flow
  • Fixed codegen closing-paren regex for _patch_model_capabilities\

Go SDK

  • Updated codegen output with \GithubToken\ field in session create/resume params
  • E2E tests for per-session auth flow

C# SDK

  • Updated codegen output with \GithubToken\ property in session create/resume params
  • E2E tests for per-session auth flow
  • Fixed AOT-safe JSON serialization in test harness (typed records instead of anonymous objects)

Codegen

  • Python codegen: fixed regex for _patch_model_capabilities\ closing paren to handle \params_dict\

Testing

All languages have E2E tests that verify:

  1. Creating a session with a per-session GitHub token
  2. The token is resolved to a Copilot user identity
  3. The session uses the per-session identity for quota lookups

Note: CI will fail until the corresponding runtime changes are deployed, since the runtime needs to accept the new \githubToken\ parameter.

SteveSandersonMS and others added 3 commits April 22, 2026 15:15
- Add githubToken to SessionConfig and ResumeSessionConfig
- Pass githubToken through in createSession/resumeSession RPC calls
- Auto-generated session.rpc.auth.getStatus() for querying session identity
- Auto-generated optional params for models.list and account.getQuota

Codegen improvements:
- Handle Zod optional params (anyOf with {not:{}}) without self-referential types
- Make function params optional when schema params are optional
- Remove unnecessary Omit<..., 'sessionId'> since sessionId is already
  stripped from generated type definitions

Test infrastructure:
- Add /copilot_internal/user mock endpoint to ReplayingCapiProxy
- Add setCopilotUserByToken to CapiProxy for per-token identity responses
- Add COPILOT_DEBUG_GITHUB_API_URL to test env for token resolution

E2E tests:
- Single token auth status verification
- Two-session isolation with different tokens
- Unauthenticated session without token
- Invalid token error handling

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
- C# SetCopilotUserByTokenAsync: use typed records with source-generated
  JsonSerializerContext instead of anonymous types (AOT requires it)
- Python codegen: fix _patch_model_capabilities regex to handle
  params_dict in addition to empty {} for optional-params methods

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1124 · ● 1.4M

Comment thread python/copilot/client.py
@SteveSandersonMS
Copy link
Copy Markdown
Contributor Author

none of the client methods (listModels, getQuota, etc.) yet surface this parameter through the public API

That is not true. All languages use those types.

Avoids falsy-check inconsistency with other SDKs — empty string
would now be sent rather than silently dropped.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated by SDK Consistency Review Agent for issue #1124 · ● 847.7K

Comment thread dotnet/src/Types.cs Outdated
Comment thread dotnet/src/Types.cs Outdated
@SteveSandersonMS SteveSandersonMS marked this pull request as ready for review April 23, 2026 12:13
@SteveSandersonMS SteveSandersonMS requested a review from a team as a code owner April 23, 2026 12:13
Copilot AI review requested due to automatic review settings April 23, 2026 12:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds per-session GitHub authentication support across the SDKs, plus test-harness plumbing and codegen updates to support new auth- and schema-related RPC surface changes.

Changes:

  • Add a session-level gitHubToken/GitHubToken/github_token to session create/resume flows (all SDK languages).
  • Extend the shared replay proxy/harness to mock /copilot_internal/user responses per bearer token for E2E tests.
  • Update codegen outputs (RPC + session events) and add per-session-auth E2E tests across languages.
Show a summary per file
File Description
test/snapshots/session/should_set_model_with_reasoningeffort.yaml New replay snapshot for reasoning-effort model selection scenario.
test/harness/replayingCapiProxy.ts Add per-token /copilot_internal/user mocking and config endpoint for tests.
scripts/codegen/utils.ts Improve schema resolution by filtering Zod’s { not: {} } optional wrapper branches.
scripts/codegen/typescript.ts TS RPC codegen updates for optional params + schema wrapper handling.
scripts/codegen/python.ts Python RPC codegen updates for optional params + models.list patch regex tweak.
scripts/codegen/go.ts Go codegen schema wrapper handling to avoid self-referential types.
scripts/codegen/csharp.ts C# codegen refactor to support custom property type resolution in polymorphic types.
python/e2e/testharness/proxy.py Add proxy helper to configure per-token /copilot_internal/user responses.
python/e2e/testharness/context.py Expose per-token user config helper via the E2E context.
python/e2e/test_per_session_auth.py New Python E2E coverage for per-session GitHub auth isolation/error cases.
python/copilot/generated/session_events.py Regenerated Python session event types (new events + usage schema alignment).
python/copilot/generated/rpc.py Regenerated Python RPC surface (auth status, optional params, permissions updates).
python/copilot/client.py Add github_token parameter to create/resume session payloads.
nodejs/test/e2e/streaming_fidelity.test.ts Update to new Node auth option naming.
nodejs/test/e2e/session.test.ts Update to new Node auth option naming.
nodejs/test/e2e/per_session_auth.test.ts New Node E2E coverage for per-session GitHub auth isolation/error cases.
nodejs/test/e2e/harness/sdkTestContext.ts Update test harness client options to new token option naming.
nodejs/test/e2e/harness/CapiProxy.ts Add proxy helper to configure per-token /copilot_internal/user responses.
nodejs/test/client.test.ts Update unit tests to new Node auth option naming + error messages.
nodejs/src/types.ts Add session-level token; rename client-level token option.
nodejs/src/generated/session-events.ts Regenerated TS session event types (new events + usage schema alignment).
nodejs/src/generated/rpc.ts Regenerated TS RPC surface (auth status, optional params, permissions updates).
nodejs/src/client.ts Wire session-level token into create/resume; rename client-level token option usage.
go/types.go Add per-session GitHub token to session config/resume config (not directly JSON-serialized).
go/rpc/generated_rpc.go Regenerated Go RPC surface (auth status, optional params, permissions updates).
go/internal/e2e/testharness/proxy.go Add proxy helper to configure per-token /copilot_internal/user responses.
go/internal/e2e/testharness/context.go Expose per-token user config helper via the Go E2E context.
go/internal/e2e/rpc_test.go Update calls to newly-parameterized server RPC methods.
go/internal/e2e/per_session_auth_test.go New Go E2E coverage for per-session GitHub auth isolation/error cases.
go/generated_session_events.go Regenerated Go session event types (new events + usage schema alignment).
go/client.go Wire session-level GitHub token into create/resume RPC requests.
dotnet/test/PerSessionAuthTests.cs New .NET E2E coverage for per-session GitHub auth isolation/error cases.
dotnet/test/Harness/E2ETestContext.cs Expose per-token user config helper via the .NET E2E context.
dotnet/test/Harness/CapiProxy.cs Add typed/AOT-safe JSON payloads for per-token user config endpoint.
dotnet/src/Types.cs Add session-level GitHubToken to public session/resume config types.
dotnet/src/Generated/SessionEvents.cs Regenerated .NET session event types (new events + usage schema alignment).
dotnet/src/Generated/Rpc.cs Regenerated .NET RPC surface (auth status, optional params, permissions updates).
dotnet/src/Client.cs Wire session-level GitHub token into create/resume request records.

Copilot's findings

  • Files reviewed: 32/38 changed files
  • Comments generated: 7

Comment on lines +260 to +270
/** True when the raw params schema uses `anyOf: [{ not: {} }, …]` — Zod's pattern for `.optional()`. */
function isParamsOptional(method: RpcMethod): boolean {
const schema = method.params;
if (!schema?.anyOf) return false;
return schema.anyOf.some(
(item) =>
typeof item === "object" &&
(item as JSONSchema7).not !== undefined &&
typeof (item as JSONSchema7).not === "object" &&
Object.keys((item as JSONSchema7).not as object).length === 0
);
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isParamsOptional can never return true as written: it checks typeof (item as JSONSchema7).not === "object" but not is a schema object in the { not: {} } pattern. This prevents optional params from being detected (and downstream signatures won’t be marked optional). Consider checking item !== null, verifying not is an object, and then testing Object.keys(not).length === 0.

Copilot uses AI. Check for mistakes.
Comment on lines 481 to 487
if (hasNonSessionParams) {
sigParams.push(`params: Omit<${paramsType}, "sessionId">`);
const optMark = isParamsOptional(value) ? "?" : "";
// sessionId is already stripped from the generated type definition,
// so no need for Omit<..., "sessionId">
sigParams.push(`params${optMark}: ${paramsType}`);
bodyArg = "{ sessionId, ...params }";
} else {
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For session RPC methods where params is optional (params?: T), the generated request body uses { sessionId, ...params }. In TS this is a type error because params is T | undefined (and can’t be spread). Use a nullish-coalesced spread (e.g., ...(params ?? {})) or keep params required and provide an overload.

Copilot uses AI. Check for mistakes.
Comment thread scripts/codegen/python.ts
Comment on lines +447 to +458
/** Detect the Zod optional params pattern: `anyOf: [{ not: {} }, { $ref }]` */
function isParamsOptional(method: RpcMethod): boolean {
const schema = method.params;
if (!schema?.anyOf) return false;
return schema.anyOf.some(
(item) =>
typeof item === "object" &&
(item as JSONSchema7).not !== undefined &&
typeof (item as JSONSchema7).not === "object" &&
Object.keys((item as JSONSchema7).not as object).length === 0
);
}
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isParamsOptional has the same logic issue as the TS generator: it checks typeof (item as JSONSchema7).not === "object", which makes the { not: {} } optional-params pattern impossible to match. This prevents emitting optional params: ... | None = None signatures for optional RPC params.

Copilot uses AI. Check for mistakes.
Comment thread scripts/codegen/utils.ts
Comment on lines +485 to +492
?.filter((item): item is JSONSchema7 => {
if (typeof item !== "object") return false;
const s = item as JSONSchema7;
// Filter out null types and `{ not: {} }` (Zod's representation of "nothing" in optional anyOf)
if (s.type === "null") return false;
if (s.not && typeof s.not === "object" && Object.keys(s.not).length === 0) return false;
return true;
});
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveObjectSchema filters anyOf/oneOf entries with typeof item !== "object", but null also has typeof === "object". If an anyOf branch is ever null, const s = item as JSONSchema7 will cause property access on null. Add an explicit item !== null guard before casting.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +65
/**
* Per-token responses for `/copilot_internal/user` endpoint.
* Key is the Bearer token (without "Bearer " prefix), value is the response body.
* When a request arrives with `Authorization: Bearer <token>`, the matching response is returned.
* If no match is found, a default response is returned.
*/
private copilotUserByToken = new Map<string, CopilotUserResponse>();
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment for copilotUserByToken says that when no token match is found “a default response is returned”, but the implementation returns a 401 with { message: "Bad credentials" }. Either update the comment to match the behavior, or implement the documented default response to avoid confusion in future tests.

Copilot uses AI. Check for mistakes.
Comment thread nodejs/src/types.ts
Comment on lines 124 to 136
/**
* 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;
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)
* When false, only explicit tokens (gitHubToken or environment variables) are used.
* @default true (but defaults to false when gitHubToken is provided)
*/
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming the public Node option from githubToken to gitHubToken is a breaking change (and existing docs in the repo still reference githubToken). Consider supporting both names (with one deprecated) to preserve backward compatibility, or keep githubToken as the public API and map it internally to the gitHubToken wire field.

See below for a potential fix:

    githubToken?: string;

    /**
     * @deprecated Use `githubToken` instead.
     */
    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, gitHubToken, or environment variables) are used.
     * @default true (but defaults to false when githubToken or gitHubToken is provided)

Copilot uses AI. Check for mistakes.
Comment thread python/e2e/testharness/context.py Outdated
Comment on lines +148 to +152
async def set_copilot_user_by_token(self, token: str, response: dict) -> None:
"""Register a per-token response for the /copilot_internal/user endpoint."""
if not self._proxy:
raise RuntimeError("Proxy not started")
await self._proxy.set_copilot_user_by_token(token, response)
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type annotation for response is just dict, while the underlying proxy API uses dict[str, Any]. Using the more precise type here keeps the harness consistent and avoids type-check noise under ty.

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

@SteveSandersonMS SteveSandersonMS changed the title feat: per-session GitHub authentication for all SDK languages Per-session GitHub authentication for all SDK languages Apr 23, 2026
@github-actions

This comment has been minimized.

@github-actions
Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅

This PR maintains excellent cross-SDK feature parity. All four language implementations receive the same additions in a consistent manner:

Features added to all SDKs

Feature Node.js Python Go .NET
Per-session GitHub token in session create/resume gitHubToken?: string github_token: str | None GitHubToken string string? GitHubToken
session.auth.getStatus RPC session.rpc.auth.getStatus() session.rpc.auth.get_status() session.RPC.Auth.GetStatus(ctx) session.Rpc.Auth.GetStatusAsync()
session.mcp.oauth.login RPC session.rpc.mcp.oauth.login(params) session.rpc.mcp.oauth.login(params) session.RPC.Mcp.Oauth.Login(ctx, params) session.Rpc.Mcp.Oauth.LoginAsync(serverName, ...)
permissions.setApproveAll
permissions.resetSessionApprovals
E2E tests for per-session auth

Naming conventions

All naming follows the expected per-language idioms:

  • TypeScript: gitHubToken, getStatus()
  • Python: github_token, get_status()
  • Go: GitHubToken, GetStatus(ctx)
  • .NET: GitHubToken, GetStatusAsync()

Breaking change note

The Node.js client-level option is renamed from githubTokengitHubToken (capital H) to match the JSON wire format and align with the Go/.NET casing. Python is unaffected since github_token has no capitalization ambiguity in snake_case. This rename is intentional and correctly documented.

No cross-SDK consistency issues found.

Generated by SDK Consistency Review Agent for issue #1124 · ● 2.3M ·

This reverts commit bf36938.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants