Skip to content

Commit 29141a4

Browse files
authored
Add session-scoped GitHub token providers (#2412)
* Add session GitHub token providers Expose lifecycle-safe GitHub credential callbacks across all six SDKs, with idiomatic APIs, tagged results, tests, and documentation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a447a8bc-8687-47ea-a545-4370979e8128 * Clarify GitHub token provider lifecycle Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a447a8bc-8687-47ea-a545-4370979e8128 * Fix GitHub token provider cleanup Release session-owned provider registrations after successful session deletion and treat empty static .NET tokens as configured for mutual-exclusion validation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a447a8bc-8687-47ea-a545-4370979e8128 * Fix Rust import formatting Order the new GitHub token re-export according to the nightly rustfmt configuration used by CI. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: a447a8bc-8687-47ea-a545-4370979e8128 --------- Copilot-Session: a447a8bc-8687-47ea-a545-4370979e8128
1 parent f0e388b commit 29141a4

54 files changed

Lines changed: 3931 additions & 175 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the fu
77

88
## [Unreleased]
99

10+
### Feature: rotating session-scoped GitHub credentials
11+
12+
All six SDKs can now acquire short-lived GitHub credentials through a session-scoped callback. The SDK registers the callback before session create or resume, maps `initial` and `refresh` requests to the owning session, and removes registrations on rollback, replacement, session close, and client close. Static per-session `gitHubToken` credentials remain supported and are mutually exclusive with the callback.
13+
14+
Token responses use the shared tagged token/cancelled shape and require `expiresIn`, expressed as the positive number of seconds remaining when the callback completes. See [github/copilot-agent-runtime#16381](https://github.com/github/copilot-agent-runtime/pull/16381) for the runtime credential-authority implementation.
15+
16+
Initial acquisition occurs during create or resume; cancellation, callback errors, and invalid credentials reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation.
17+
1018
### Feature: extensions can request sensitive environment variables
1119

1220
Copilot CLI extensions can now ask for named sensitive environment variables when they join a session. `joinSession()` accepts a `requestedEnvironmentVariables` option listing the variable names the extension needs. The CLI shows a permission prompt naming the extension and the exact variables requested. On approval, only those variables reach that extension and their values are written into the extension process's `process.env` before `joinSession()` resolves. On denial, `joinSession()` rejects, the extension does not load, and its tools never reach the model.

docs/auth/authenticate.md

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,131 @@ const client = new CopilotClient({
262262

263263
For more information, see [GitHub OAuth](../setup/github-oauth.md).
264264

265+
## Rotating session-scoped GitHub tokens
266+
267+
For multi-user services and integrations, set a token provider on each session instead of storing one long-lived token. The runtime calls the provider for the effective GitHub host and identifies the request as `initial` or `refresh`. The session ID is absent only when a cloud session has not received its ID yet.
268+
269+
Return a tagged token result or an explicit cancellation. Every token result must include `expiresIn`: the positive number of seconds remaining when the callback completes. Production GitHub tokens typically last eight hours, so `8 * 60 * 60` is a common value. Do not set both the static per-session token and the provider.
270+
271+
<details open>
272+
<summary><strong>TypeScript</strong></summary>
273+
274+
<!-- docs-validate: skip -->
275+
```typescript
276+
const session = await client.createSession({
277+
gitHubTokenProvider: async ({ host, sessionId, reason }) => {
278+
const token = await acquireGitHubToken({ host, sessionId, reason });
279+
return {
280+
kind: "token",
281+
accessToken: token.value,
282+
expiresIn: token.secondsRemaining,
283+
};
284+
},
285+
});
286+
```
287+
288+
</details>
289+
<details>
290+
<summary><strong>Python</strong></summary>
291+
292+
<!-- docs-validate: skip -->
293+
```python
294+
async def provide_github_token(args):
295+
token = await acquire_github_token(
296+
host=args["host"],
297+
session_id=args["session_id"],
298+
reason=args["reason"],
299+
)
300+
return {
301+
"kind": "token",
302+
"accessToken": token.value,
303+
"expiresIn": token.seconds_remaining,
304+
}
305+
306+
307+
session = await client.create_session(github_token_provider=provide_github_token)
308+
```
309+
310+
</details>
311+
<details>
312+
<summary><strong>Go</strong></summary>
313+
314+
<!-- docs-validate: skip -->
315+
```go
316+
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
317+
GitHubTokenProvider: func(args copilot.GitHubTokenProviderArgs) (*copilot.GitHubTokenProviderResult, error) {
318+
token, secondsRemaining, err := acquireGitHubToken(args.Host, args.SessionID, args.Reason)
319+
if err != nil {
320+
return nil, err
321+
}
322+
return copilot.GitHubTokenResult(&copilot.GitHubToken{
323+
AccessToken: token,
324+
ExpiresIn: secondsRemaining,
325+
}), nil
326+
},
327+
})
328+
```
329+
330+
</details>
331+
<details>
332+
<summary><strong>.NET</strong></summary>
333+
334+
<!-- docs-validate: skip -->
335+
```csharp
336+
await using var session = await client.CreateSessionAsync(new SessionConfig
337+
{
338+
GitHubTokenProvider = async args =>
339+
{
340+
var token = await AcquireGitHubTokenAsync(args.Host, args.SessionId, args.Reason);
341+
return GitHubTokenProviderResult.FromToken(new GitHubToken
342+
{
343+
AccessToken = token.Value,
344+
ExpiresIn = token.SecondsRemaining,
345+
});
346+
},
347+
});
348+
```
349+
350+
</details>
351+
<details>
352+
<summary><strong>Java</strong></summary>
353+
354+
<!-- docs-validate: skip -->
355+
```java
356+
var session = client.createSession(new SessionConfig()
357+
.setGitHubTokenProvider(args ->
358+
acquireGitHubToken(args.host(), args.sessionId(), args.reason())
359+
.thenApply(token -> GitHubTokenProviderResult.token(
360+
token.value(), token.secondsRemaining())))
361+
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
362+
).get();
363+
```
364+
365+
</details>
366+
<details>
367+
<summary><strong>Rust</strong></summary>
368+
369+
<!-- docs-validate: skip -->
370+
```rust
371+
let provider = Arc::new(|args: GitHubTokenProviderArgs| async move {
372+
let token = acquire_github_token(&args.host, args.session_id.as_ref(), args.reason).await?;
373+
Ok(GitHubTokenProviderResult::Token(GitHubToken::new(
374+
token.value,
375+
token.seconds_remaining,
376+
)))
377+
});
378+
379+
let session = client
380+
.create_session(SessionConfig::default().with_github_token_provider(provider))
381+
.await?;
382+
```
383+
384+
</details>
385+
386+
The runtime performs the `initial` acquisition as part of session creation or resume. A cancelled acquisition, provider error, invalid response, or token without a stable account identity rejects the create or resume operation. The runtime does not fall back to ambient authentication.
387+
388+
After the session is established, the runtime performs async preflight before each credential-consuming operation. It requests a `refresh` when the current token has one hour or less remaining. Idle sessions are not refreshed until their next credential-consuming operation. The runtime does not use background timers, rejection-driven replay, 401/403 challenge propagation, or upscope for this callback.
389+
265390
## Environment variables
266391

267392
For automation, CI/CD pipelines, and server-to-server scenarios, you can authenticate using environment variables.

docs/setup/multi-tenancy.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ This guide is a sister to [Scaling and multi-tenancy](./scaling.md). Use that gu
2424
| `baseDirectory` | Isolating `COPILOT_HOME` per runtime instance | Ignored when connecting to an existing runtime. |
2525
| `sessionFs` | Routing session filesystem storage off local disk | Pair with per-session filesystem providers. |
2626
| `RuntimeConnection.forUri(url)` | Sharing one already-running runtime | Language names vary; see samples below. |
27-
| Per-session `gitHubToken` | Scoping auth to the requesting user | Prefer this over a single shared user token. |
27+
| Per-session GitHub token or provider | Scoping auth to the requesting user | Prefer a rotating provider for short-lived credentials; use a static `gitHubToken` only when rotation is unnecessary. |
28+
29+
For callback-backed credentials, see [Rotating session-scoped GitHub tokens](../auth/authenticate.md#rotating-session-scoped-github-tokens). Each session owns its provider registration, so concurrent sessions can use different GitHub hosts and accounts without sharing callback state.
2830

2931
### `mode: "empty"`
3032

dotnet/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ Create a new conversation session.
133133
- `InfiniteSessions` - Configure automatic context compaction (see below)
134134
- `WorkingDirectory` - Working directory for the session. When not set, the runtime uses its own process working directory.
135135
- `EnableSessionStore` - Enables the cross-session store for search and retrieval across sessions. When unset in `CopilotClientMode.CopilotCli`, the runtime default applies (enabled). In `CopilotClientMode.Empty`, defaults to disabled.
136+
- `GitHubTokenProvider` - Acquires session-scoped GitHub tokens on demand. Return `GitHubTokenProviderResult.FromToken` with a positive `ExpiresIn` value (production GitHub tokens typically use `8 * 60 * 60` seconds), or `GitHubTokenProviderResult.Cancel()`. Cannot be combined with `GitHubToken`.
136137
- `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. When omitted, permission requests are emitted as events and left pending for manual resolution. `PermissionHandler.ApproveAll` approves requests when managed settings are disabled and throws when `EnableManagedSettings` is true. Custom handlers can inspect `ManagedApprovalRequired` for human-facing confirmation logic. See [Permission Handling](#permission-handling) section.
137138
- `OnUserInputRequest` - Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
138139
- `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
@@ -144,6 +145,24 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i
144145
**ResumeSessionConfig:**
145146

146147
- `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section.
148+
- `GitHubTokenProvider` - Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`.
149+
150+
```csharp
151+
await using var session = await client.CreateSessionAsync(new SessionConfig
152+
{
153+
GitHubTokenProvider = async args =>
154+
{
155+
var token = await AcquireTokenAsync(args.Host);
156+
return GitHubTokenProviderResult.FromToken(new GitHubToken
157+
{
158+
AccessToken = token,
159+
ExpiresIn = 8 * 60 * 60
160+
});
161+
}
162+
});
163+
```
164+
165+
Initial acquisition runs during session creation or resume. Cancellation, provider errors, and invalid token responses reject that operation instead of falling back to ambient authentication. Idle sessions refresh only before their next credential-consuming operation; there is no background refresh timer.
147166

148167
##### `PingAsync(string? message = null): Task<PingResponse>`
149168

0 commit comments

Comments
 (0)