Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ See [GitHub Releases](https://github.com/github/copilot-sdk/releases) for the fu

## [Unreleased]

### Feature: rotating session-scoped GitHub credentials

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.

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.

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.

### Feature: extensions can request sensitive environment variables

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.
Expand Down
125 changes: 125 additions & 0 deletions docs/auth/authenticate.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,131 @@ const client = new CopilotClient({

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

## Rotating session-scoped GitHub tokens

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.

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.
Comment thread
roji marked this conversation as resolved.

<details open>
<summary><strong>TypeScript</strong></summary>

<!-- docs-validate: skip -->
```typescript
const session = await client.createSession({
gitHubTokenProvider: async ({ host, sessionId, reason }) => {
const token = await acquireGitHubToken({ host, sessionId, reason });
return {
kind: "token",
accessToken: token.value,
expiresIn: token.secondsRemaining,
};
},
});
```

</details>
<details>
<summary><strong>Python</strong></summary>

<!-- docs-validate: skip -->
```python
async def provide_github_token(args):
token = await acquire_github_token(
host=args["host"],
session_id=args["session_id"],
reason=args["reason"],
)
return {
"kind": "token",
"accessToken": token.value,
"expiresIn": token.seconds_remaining,
}


session = await client.create_session(github_token_provider=provide_github_token)
```

</details>
<details>
<summary><strong>Go</strong></summary>

<!-- docs-validate: skip -->
```go
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
GitHubTokenProvider: func(args copilot.GitHubTokenProviderArgs) (*copilot.GitHubTokenProviderResult, error) {
token, secondsRemaining, err := acquireGitHubToken(args.Host, args.SessionID, args.Reason)
if err != nil {
return nil, err
}
return copilot.GitHubTokenResult(&copilot.GitHubToken{
AccessToken: token,
ExpiresIn: secondsRemaining,
}), nil
},
})
```

</details>
<details>
<summary><strong>.NET</strong></summary>

<!-- docs-validate: skip -->
```csharp
await using var session = await client.CreateSessionAsync(new SessionConfig
{
GitHubTokenProvider = async args =>
{
var token = await AcquireGitHubTokenAsync(args.Host, args.SessionId, args.Reason);
return GitHubTokenProviderResult.FromToken(new GitHubToken
{
AccessToken = token.Value,
ExpiresIn = token.SecondsRemaining,
});
},
});
```

</details>
<details>
<summary><strong>Java</strong></summary>

<!-- docs-validate: skip -->
```java
var session = client.createSession(new SessionConfig()
.setGitHubTokenProvider(args ->
acquireGitHubToken(args.host(), args.sessionId(), args.reason())
.thenApply(token -> GitHubTokenProviderResult.token(
token.value(), token.secondsRemaining())))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
```

</details>
<details>
<summary><strong>Rust</strong></summary>

<!-- docs-validate: skip -->
```rust
let provider = Arc::new(|args: GitHubTokenProviderArgs| async move {
let token = acquire_github_token(&args.host, args.session_id.as_ref(), args.reason).await?;
Ok(GitHubTokenProviderResult::Token(GitHubToken::new(
token.value,
token.seconds_remaining,
)))
});

let session = client
.create_session(SessionConfig::default().with_github_token_provider(provider))
.await?;
```

</details>

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.

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.

## Environment variables

For automation, CI/CD pipelines, and server-to-server scenarios, you can authenticate using environment variables.
Expand Down
4 changes: 3 additions & 1 deletion docs/setup/multi-tenancy.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ This guide is a sister to [Scaling and multi-tenancy](./scaling.md). Use that gu
| `baseDirectory` | Isolating `COPILOT_HOME` per runtime instance | Ignored when connecting to an existing runtime. |
| `sessionFs` | Routing session filesystem storage off local disk | Pair with per-session filesystem providers. |
| `RuntimeConnection.forUri(url)` | Sharing one already-running runtime | Language names vary; see samples below. |
| Per-session `gitHubToken` | Scoping auth to the requesting user | Prefer this over a single shared user token. |
| 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. |

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.

### `mode: "empty"`

Expand Down
19 changes: 19 additions & 0 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ Create a new conversation session.
- `InfiniteSessions` - Configure automatic context compaction (see below)
- `WorkingDirectory` - Working directory for the session. When not set, the runtime uses its own process working directory.
- `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.
- `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`.
- `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.
- `OnUserInputRequest` - Handler for user input requests from the agent (enables ask_user tool). See [User Input Requests](#user-input-requests) section.
- `Hooks` - Hook handlers for session lifecycle events. See [Session Hooks](#session-hooks) section.
Expand All @@ -144,6 +145,24 @@ Resume an existing session. Returns the session with `WorkspacePath` populated i
**ResumeSessionConfig:**

- `OnPermissionRequest` - Optional handler called before each tool execution to approve or deny it. See [Permission Handling](#permission-handling) section.
- `GitHubTokenProvider` - Replaces the session-scoped token provider when resuming. Cannot be combined with `GitHubToken`.

```csharp
await using var session = await client.CreateSessionAsync(new SessionConfig
{
GitHubTokenProvider = async args =>
{
var token = await AcquireTokenAsync(args.Host);
return GitHubTokenProviderResult.FromToken(new GitHubToken
{
AccessToken = token,
ExpiresIn = 8 * 60 * 60
});
}
});
```

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.

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

Expand Down
Loading
Loading