diff --git a/docs/auth/index.md b/docs/auth/index.md index 069556a9b..5b2f667da 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -110,7 +110,7 @@ Use an OAuth GitHub App to authenticate users through your application and pass **How it works:** 1. User authorizes your OAuth GitHub App 2. Your app receives a user access token (`gho_` or `ghu_` prefix) -3. Pass the token to the SDK via `githubToken` option +3. Pass the token to the SDK via `gitHubToken` option **SDK Configuration:** @@ -121,7 +121,7 @@ Use an OAuth GitHub App to authenticate users through your application and pass import { CopilotClient } from "@github/copilot-sdk"; const client = new CopilotClient({ - githubToken: userAccessToken, // Token from OAuth flow + gitHubToken: userAccessToken, // Token from OAuth flow useLoggedInUser: false, // Don't use stored CLI credentials }); ``` @@ -299,7 +299,7 @@ BYOK allows you to use your own API keys from model providers like Azure AI Foun When multiple authentication methods are available, the SDK uses them in this priority order: -1. **Explicit `githubToken`** - Token passed directly to SDK constructor +1. **Explicit `gitHubToken`** - Token passed directly to SDK constructor 2. **HMAC key** - `CAPI_HMAC_KEY` or `COPILOT_HMAC_KEY` environment variables 3. **Direct API token** - `GITHUB_COPILOT_API_TOKEN` with `COPILOT_API_URL` 4. **Environment variable tokens** - `COPILOT_GITHUB_TOKEN` → `GH_TOKEN` → `GITHUB_TOKEN` diff --git a/docs/setup/backend-services.md b/docs/setup/backend-services.md index cc5a055b4..a2c8620ab 100644 --- a/docs/setup/backend-services.md +++ b/docs/setup/backend-services.md @@ -290,7 +290,7 @@ Pass individual user tokens when creating sessions. See [GitHub OAuth](./github- app.post("/chat", authMiddleware, async (req, res) => { const client = new CopilotClient({ cliUrl: "localhost:4321", - githubToken: req.user.githubToken, + gitHubToken: req.user.githubToken, useLoggedInUser: false, }); diff --git a/docs/setup/github-oauth.md b/docs/setup/github-oauth.md index 553dde1cb..0f2be236e 100644 --- a/docs/setup/github-oauth.md +++ b/docs/setup/github-oauth.md @@ -26,7 +26,7 @@ sequenceDiagram GH-->>App: Access token (gho_xxx) App->>SDK: Create client with token - SDK->>CLI: Start with githubToken + SDK->>CLI: Start with gitHubToken CLI->>API: Request (as user) API-->>CLI: Response CLI-->>SDK: Result @@ -124,7 +124,7 @@ import { CopilotClient } from "@github/copilot-sdk"; // Create a client for an authenticated user function createClientForUser(userToken: string): CopilotClient { return new CopilotClient({ - githubToken: userToken, + gitHubToken: userToken, useLoggedInUser: false, // Don't fall back to CLI login }); } @@ -373,7 +373,7 @@ For GitHub Enterprise Managed Users, the flow is identical — EMU users authent // No special SDK configuration needed for EMU // Enterprise policies are enforced server-side by GitHub const client = new CopilotClient({ - githubToken: emuUserToken, // Works the same as regular tokens + gitHubToken: emuUserToken, // Works the same as regular tokens useLoggedInUser: false, }); ``` @@ -438,7 +438,7 @@ const clients = new Map(); function getClientForUser(userId: string, token: string): CopilotClient { if (!clients.has(userId)) { clients.set(userId, new CopilotClient({ - githubToken: token, + gitHubToken: token, useLoggedInUser: false, })); } diff --git a/docs/troubleshooting/compatibility.md b/docs/troubleshooting/compatibility.md index 44632ab6a..aed5286bf 100644 --- a/docs/troubleshooting/compatibility.md +++ b/docs/troubleshooting/compatibility.md @@ -55,7 +55,7 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b | Create workspace file | `session.rpc.workspace.createFile()` | Create file in workspace | | **Authentication** | | | | Get auth status | `getAuthStatus()` | Check login state | -| Use token | `githubToken` option | Programmatic auth | +| Use token | `gitHubToken` option | Programmatic auth | | **Connectivity** | | | | Ping | `client.ping()` | Health check with server timestamp | | Get server status | `client.getStatus()` | Protocol version and server info | diff --git a/docs/troubleshooting/debugging.md b/docs/troubleshooting/debugging.md index 802798b21..4d060cdd3 100644 --- a/docs/troubleshooting/debugging.md +++ b/docs/troubleshooting/debugging.md @@ -268,7 +268,7 @@ var client = new CopilotClient(new CopilotClientOptions ```typescript const client = new CopilotClient({ - githubToken: process.env.GITHUB_TOKEN, + gitHubToken: process.env.GITHUB_TOKEN, }); ``` diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 3a161a391..9b3170e97 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -513,7 +513,8 @@ public async Task CreateSessionAsync(SessionConfig config, Cance RequestElicitation: config.OnElicitationRequest != null, Traceparent: traceparent, Tracestate: tracestate, - ModelCapabilities: config.ModelCapabilities); + ModelCapabilities: config.ModelCapabilities, + GitHubToken: config.GitHubToken); var response = await InvokeRpcAsync( connection.Rpc, "session.create", [request], cancellationToken); @@ -638,7 +639,8 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes RequestElicitation: config.OnElicitationRequest != null, Traceparent: traceparent, Tracestate: tracestate, - ModelCapabilities: config.ModelCapabilities); + ModelCapabilities: config.ModelCapabilities, + GitHubToken: config.GitHubToken); var response = await InvokeRpcAsync( connection.Rpc, "session.resume", [request], cancellationToken); @@ -1656,7 +1658,8 @@ internal record CreateSessionRequest( bool? RequestElicitation = null, string? Traceparent = null, string? Tracestate = null, - ModelCapabilitiesOverride? ModelCapabilities = null); + ModelCapabilitiesOverride? ModelCapabilities = null, + string? GitHubToken = null); internal record ToolDefinition( string Name, @@ -1711,7 +1714,8 @@ internal record ResumeSessionRequest( bool? RequestElicitation = null, string? Traceparent = null, string? Tracestate = null, - ModelCapabilitiesOverride? ModelCapabilities = null); + ModelCapabilitiesOverride? ModelCapabilities = null, + string? GitHubToken = null); internal record ResumeSessionResponse( string SessionId, diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 8de5b2fd4..7fa3adeec 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -129,7 +129,7 @@ public sealed class ModelPolicy /// Usage terms or conditions for this model. [JsonPropertyName("terms")] - public string Terms { get; set; } = string.Empty; + public string? Terms { get; set; } } /// RPC data type for Model operations. @@ -172,6 +172,14 @@ public sealed class ModelList public IList Models { get => field ??= []; set; } } +/// RPC data type for ModelsList operations. +internal sealed class ModelsListRequest +{ + /// GitHub token for per-user model listing. When provided, resolves this token to determine the user's Copilot plan and available models instead of using the global auth. + [JsonPropertyName("gitHubToken")] + public string? GitHubToken { get; set; } +} + /// RPC data type for Tool operations. public sealed class Tool { @@ -258,6 +266,14 @@ public sealed class AccountGetQuotaResult public IDictionary QuotaSnapshots { get => field ??= new Dictionary(); set; } } +/// RPC data type for AccountGetQuota operations. +internal sealed class AccountGetQuotaRequest +{ + /// GitHub token for per-user quota lookup. When provided, resolves this token to determine the user's quota instead of using the global auth. + [JsonPropertyName("gitHubToken")] + public string? GitHubToken { get; set; } +} + /// RPC data type for DiscoveredMcpServer operations. public sealed class DiscoveredMcpServer { @@ -266,7 +282,9 @@ public sealed class DiscoveredMcpServer public bool Enabled { get; set; } /// Server name (config key). - [RegularExpression("^[0-9a-zA-Z_.@-]+(\\/[0-9a-zA-Z_.@-]+)*$")] + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; @@ -311,7 +329,9 @@ internal sealed class McpConfigAddRequest public object Config { get; set; } = null!; /// Unique name for the MCP server. - [RegularExpression("^[0-9a-zA-Z_.@-]+(\\/[0-9a-zA-Z_.@-]+)*$")] + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; } @@ -324,7 +344,9 @@ internal sealed class McpConfigUpdateRequest public object Config { get; set; } = null!; /// Name of the MCP server to update. - [RegularExpression("^[0-9a-zA-Z_.@-]+(\\/[0-9a-zA-Z_.@-]+)*$")] + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; } @@ -333,11 +355,29 @@ internal sealed class McpConfigUpdateRequest internal sealed class McpConfigRemoveRequest { /// Name of the MCP server to remove. - [RegularExpression("^[0-9a-zA-Z_.@-]+(\\/[0-9a-zA-Z_.@-]+)*$")] + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; } +/// RPC data type for McpConfigEnable operations. +internal sealed class McpConfigEnableRequest +{ + /// Names of MCP servers to enable. Each server is removed from the persisted disabled list so new sessions spawn it. Unknown or already-enabled names are ignored. + [JsonPropertyName("names")] + public IList Names { get => field ??= []; set; } +} + +/// RPC data type for McpConfigDisable operations. +internal sealed class McpConfigDisableRequest +{ + /// Names of MCP servers to disable. Each server is added to the persisted disabled list so new sessions skip it. Already-disabled names are ignored. Active sessions keep their current connections until they end. + [JsonPropertyName("names")] + public IList Names { get => field ??= []; set; } +} + /// RPC data type for ServerSkill operations. public sealed class ServerSkill { @@ -478,6 +518,42 @@ internal sealed class LogRequest public string? Url { get; set; } } +/// RPC data type for SessionAuthStatus operations. +public sealed class SessionAuthStatus +{ + /// Authentication type. + [JsonPropertyName("authType")] + public AuthInfoType? AuthType { get; set; } + + /// Copilot plan tier (e.g., individual_pro, business). + [JsonPropertyName("copilotPlan")] + public string? CopilotPlan { get; set; } + + /// Authentication host URL. + [JsonPropertyName("host")] + public string? Host { get; set; } + + /// Whether the session has resolved authentication. + [JsonPropertyName("isAuthenticated")] + public bool IsAuthenticated { get; set; } + + /// Authenticated login/username, if available. + [JsonPropertyName("login")] + public string? Login { get; set; } + + /// Human-readable authentication status description. + [JsonPropertyName("statusMessage")] + public string? StatusMessage { get; set; } +} + +/// RPC data type for SessionAuthGetStatus operations. +internal sealed class SessionAuthGetStatusRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// RPC data type for CurrentModel operations. public sealed class CurrentModel { @@ -1087,7 +1163,9 @@ public sealed class McpServer public string? Error { get; set; } /// Server name (config key). - [RegularExpression("^[0-9a-zA-Z_.@-]+(\\/[0-9a-zA-Z_.@-]+)*$")] + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] [JsonPropertyName("name")] public string Name { get; set; } = string.Empty; @@ -1123,7 +1201,9 @@ internal sealed class SessionMcpListRequest internal sealed class McpEnableRequest { /// Name of the MCP server to enable. - [RegularExpression("^[0-9a-zA-Z_.@-]+(\\/[0-9a-zA-Z_.@-]+)*$")] + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] [JsonPropertyName("serverName")] public string ServerName { get; set; } = string.Empty; @@ -1137,7 +1217,9 @@ internal sealed class McpEnableRequest internal sealed class McpDisableRequest { /// Name of the MCP server to disable. - [RegularExpression("^[0-9a-zA-Z_.@-]+(\\/[0-9a-zA-Z_.@-]+)*$")] + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] [JsonPropertyName("serverName")] public string ServerName { get; set; } = string.Empty; @@ -1155,6 +1237,43 @@ internal sealed class SessionMcpReloadRequest public string SessionId { get; set; } = string.Empty; } +/// RPC data type for McpOauthLogin operations. +[Experimental(Diagnostics.Experimental)] +public sealed class McpOauthLoginResult +{ + /// URL the caller should open in a browser to complete OAuth. Omitted when cached tokens were still valid and no browser interaction was needed — the server is already reconnected in that case. When present, the runtime starts the callback listener before returning and continues the flow in the background; completion is signaled via session.mcp_server_status_changed. + [JsonPropertyName("authorizationUrl")] + public string? AuthorizationUrl { get; set; } +} + +/// RPC data type for McpOauthLogin operations. +[Experimental(Diagnostics.Experimental)] +internal sealed class McpOauthLoginRequest +{ + /// Optional override for the body text shown on the OAuth loopback callback success page. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass surface-specific copy telling the user where to return. + [JsonPropertyName("callbackSuccessMessage")] + public string? CallbackSuccessMessage { get; set; } + + /// Optional override for the OAuth client display name shown on the consent screen. Applies to newly registered dynamic clients only — existing registrations keep the name they were created with. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass their own surface-specific label so the consent screen matches the product the user sees. + [JsonPropertyName("clientName")] + public string? ClientName { get; set; } + + /// When true, clears any cached OAuth token for the server and runs a full new authorization. Use when the user explicitly wants to switch accounts or believes their session is stuck. + [JsonPropertyName("forceReauth")] + public bool? ForceReauth { get; set; } + + /// Name of the remote MCP server to authenticate. + [RegularExpression("^[^\\x00-\\x1f/\\x7f-\\x9f}]+(?:\\/[^\\x00-\\x1f/\\x7f-\\x9f}]+)*$")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Safe for generated string properties: JSON Schema minLength/maxLength map to string length validation, not reflection over trimmed Count members")] + [MinLength(1)] + [JsonPropertyName("serverName")] + public string ServerName { get; set; } = string.Empty; + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// RPC data type for Plugin operations. public sealed class Plugin { @@ -1403,6 +1522,8 @@ public sealed class PermissionRequestResult TypeDiscriminatorPropertyName = "kind", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [JsonDerivedType(typeof(PermissionDecisionApproved), "approved")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForSession), "approved-for-session")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForLocation), "approved-for-location")] [JsonDerivedType(typeof(PermissionDecisionDeniedByRules), "denied-by-rules")] [JsonDerivedType(typeof(PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser), "denied-no-approval-rule-and-could-not-request-from-user")] [JsonDerivedType(typeof(PermissionDecisionDeniedInteractivelyByUser), "denied-interactively-by-user")] @@ -1424,6 +1545,208 @@ public partial class PermissionDecisionApproved : PermissionDecision public override string Kind => "approved"; } +/// The approval to add as a session-scoped rule. +/// Polymorphic base type discriminated by kind. +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(PermissionDecisionApprovedForSessionApprovalCommands), "commands")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForSessionApprovalWrite), "write")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForSessionApprovalMcp), "mcp")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForSessionApprovalMcpSampling), "mcp-sampling")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForSessionApprovalMemory), "memory")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForSessionApprovalCustomTool), "custom-tool")] +public partial class PermissionDecisionApprovedForSessionApproval +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} + + +/// The commands variant of . +public partial class PermissionDecisionApprovedForSessionApprovalCommands : PermissionDecisionApprovedForSessionApproval +{ + /// + [JsonIgnore] + public override string Kind => "commands"; + + /// Gets or sets the commandIdentifiers value. + [JsonPropertyName("commandIdentifiers")] + public required IList CommandIdentifiers { get; set; } +} + +/// The write variant of . +public partial class PermissionDecisionApprovedForSessionApprovalWrite : PermissionDecisionApprovedForSessionApproval +{ + /// + [JsonIgnore] + public override string Kind => "write"; +} + +/// The mcp variant of . +public partial class PermissionDecisionApprovedForSessionApprovalMcp : PermissionDecisionApprovedForSessionApproval +{ + /// + [JsonIgnore] + public override string Kind => "mcp"; + + /// Gets or sets the serverName value. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// Gets or sets the toolName value. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } +} + +/// The mcp-sampling variant of . +public partial class PermissionDecisionApprovedForSessionApprovalMcpSampling : PermissionDecisionApprovedForSessionApproval +{ + /// + [JsonIgnore] + public override string Kind => "mcp-sampling"; + + /// Gets or sets the serverName value. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + +/// The memory variant of . +public partial class PermissionDecisionApprovedForSessionApprovalMemory : PermissionDecisionApprovedForSessionApproval +{ + /// + [JsonIgnore] + public override string Kind => "memory"; +} + +/// The custom-tool variant of . +public partial class PermissionDecisionApprovedForSessionApprovalCustomTool : PermissionDecisionApprovedForSessionApproval +{ + /// + [JsonIgnore] + public override string Kind => "custom-tool"; + + /// Gets or sets the toolName value. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } +} + +/// The approved-for-session variant of . +public partial class PermissionDecisionApprovedForSession : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approved-for-session"; + + /// The approval to add as a session-scoped rule. + [JsonPropertyName("approval")] + public required PermissionDecisionApprovedForSessionApproval Approval { get; set; } +} + +/// The approval to persist for this location. +/// Polymorphic base type discriminated by kind. +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(PermissionDecisionApprovedForLocationApprovalCommands), "commands")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForLocationApprovalWrite), "write")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForLocationApprovalMcp), "mcp")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForLocationApprovalMcpSampling), "mcp-sampling")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForLocationApprovalMemory), "memory")] +[JsonDerivedType(typeof(PermissionDecisionApprovedForLocationApprovalCustomTool), "custom-tool")] +public partial class PermissionDecisionApprovedForLocationApproval +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} + + +/// The commands variant of . +public partial class PermissionDecisionApprovedForLocationApprovalCommands : PermissionDecisionApprovedForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "commands"; + + /// Gets or sets the commandIdentifiers value. + [JsonPropertyName("commandIdentifiers")] + public required IList CommandIdentifiers { get; set; } +} + +/// The write variant of . +public partial class PermissionDecisionApprovedForLocationApprovalWrite : PermissionDecisionApprovedForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "write"; +} + +/// The mcp variant of . +public partial class PermissionDecisionApprovedForLocationApprovalMcp : PermissionDecisionApprovedForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "mcp"; + + /// Gets or sets the serverName value. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// Gets or sets the toolName value. + [JsonPropertyName("toolName")] + public string? ToolName { get; set; } +} + +/// The mcp-sampling variant of . +public partial class PermissionDecisionApprovedForLocationApprovalMcpSampling : PermissionDecisionApprovedForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "mcp-sampling"; + + /// Gets or sets the serverName value. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } +} + +/// The memory variant of . +public partial class PermissionDecisionApprovedForLocationApprovalMemory : PermissionDecisionApprovedForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "memory"; +} + +/// The custom-tool variant of . +public partial class PermissionDecisionApprovedForLocationApprovalCustomTool : PermissionDecisionApprovedForLocationApproval +{ + /// + [JsonIgnore] + public override string Kind => "custom-tool"; + + /// Gets or sets the toolName value. + [JsonPropertyName("toolName")] + public required string ToolName { get; set; } +} + +/// The approved-for-location variant of . +public partial class PermissionDecisionApprovedForLocation : PermissionDecision +{ + /// + [JsonIgnore] + public override string Kind => "approved-for-location"; + + /// The approval to persist for this location. + [JsonPropertyName("approval")] + public required PermissionDecisionApprovedForLocationApproval Approval { get; set; } + + /// The location key (git root or cwd) to persist the approval to. + [JsonPropertyName("locationKey")] + public required string LocationKey { get; set; } +} + /// The denied-by-rules variant of . public partial class PermissionDecisionDeniedByRules : PermissionDecision { @@ -1433,7 +1756,7 @@ public partial class PermissionDecisionDeniedByRules : PermissionDecision /// Rules that denied the request. [JsonPropertyName("rules")] - public required object[] Rules { get; set; } + public required IList Rules { get; set; } } /// The denied-no-approval-rule-and-could-not-request-from-user variant of . @@ -1507,6 +1830,42 @@ internal sealed class PermissionDecisionRequest public string SessionId { get; set; } = string.Empty; } +/// RPC data type for PermissionsSetApproveAll operations. +public sealed class PermissionsSetApproveAllResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// RPC data type for PermissionsSetApproveAll operations. +internal sealed class PermissionsSetApproveAllRequest +{ + /// Whether to auto-approve all tool permission requests. + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// RPC data type for PermissionsResetSessionApprovals operations. +public sealed class PermissionsResetSessionApprovalsResult +{ + /// Whether the operation succeeded. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// RPC data type for PermissionsResetSessionApprovals operations. +internal sealed class PermissionsResetSessionApprovalsRequest +{ + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// RPC data type for ShellExec operations. public sealed class ShellExecResult { @@ -2097,6 +2456,34 @@ public enum SessionLogLevel } +/// Authentication type. +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum AuthInfoType +{ + /// The hmac variant. + [JsonStringEnumMemberName("hmac")] + Hmac, + /// The env variant. + [JsonStringEnumMemberName("env")] + Env, + /// The user variant. + [JsonStringEnumMemberName("user")] + User, + /// The gh-cli variant. + [JsonStringEnumMemberName("gh-cli")] + GhCli, + /// The api-key variant. + [JsonStringEnumMemberName("api-key")] + ApiKey, + /// The token variant. + [JsonStringEnumMemberName("token")] + Token, + /// The copilot-api-token variant. + [JsonStringEnumMemberName("copilot-api-token")] + CopilotApiToken, +} + + /// The agent mode. Valid values: "interactive", "plan", "autopilot". [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionMode @@ -2374,9 +2761,10 @@ internal ServerModelsApi(JsonRpc rpc) } /// Calls "models.list". - public async Task ListAsync(CancellationToken cancellationToken = default) + public async Task ListAsync(string? gitHubToken = null, CancellationToken cancellationToken = default) { - return await CopilotClient.InvokeRpcAsync(_rpc, "models.list", [], cancellationToken); + var request = new ModelsListRequest { GitHubToken = gitHubToken }; + return await CopilotClient.InvokeRpcAsync(_rpc, "models.list", [request], cancellationToken); } } @@ -2409,9 +2797,10 @@ internal ServerAccountApi(JsonRpc rpc) } /// Calls "account.getQuota". - public async Task GetQuotaAsync(CancellationToken cancellationToken = default) + public async Task GetQuotaAsync(string? gitHubToken = null, CancellationToken cancellationToken = default) { - return await CopilotClient.InvokeRpcAsync(_rpc, "account.getQuota", [], cancellationToken); + var request = new AccountGetQuotaRequest { GitHubToken = gitHubToken }; + return await CopilotClient.InvokeRpcAsync(_rpc, "account.getQuota", [request], cancellationToken); } } @@ -2473,6 +2862,20 @@ public async Task RemoveAsync(string name, CancellationToken cancellationToken = var request = new McpConfigRemoveRequest { Name = name }; await CopilotClient.InvokeRpcAsync(_rpc, "mcp.config.remove", [request], cancellationToken); } + + /// Calls "mcp.config.enable". + public async Task EnableAsync(IList names, CancellationToken cancellationToken = default) + { + var request = new McpConfigEnableRequest { Names = names }; + await CopilotClient.InvokeRpcAsync(_rpc, "mcp.config.enable", [request], cancellationToken); + } + + /// Calls "mcp.config.disable". + public async Task DisableAsync(IList names, CancellationToken cancellationToken = default) + { + var request = new McpConfigDisableRequest { Names = names }; + await CopilotClient.InvokeRpcAsync(_rpc, "mcp.config.disable", [request], cancellationToken); + } } /// Provides server-scoped Skills APIs. @@ -2562,6 +2965,7 @@ internal SessionRpc(JsonRpc rpc, string sessionId) { _rpc = rpc; _sessionId = sessionId; + Auth = new AuthApi(rpc, sessionId); Model = new ModelApi(rpc, sessionId); Mode = new ModeApi(rpc, sessionId); Name = new NameApi(rpc, sessionId); @@ -2583,6 +2987,9 @@ internal SessionRpc(JsonRpc rpc, string sessionId) Usage = new UsageApi(rpc, sessionId); } + /// Auth APIs. + public AuthApi Auth { get; } + /// Model APIs. public ModelApi Model { get; } @@ -2648,6 +3055,26 @@ public async Task LogAsync(string message, SessionLogLevel? level = n } } +/// Provides session-scoped Auth APIs. +public sealed class AuthApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal AuthApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.auth.getStatus". + public async Task GetStatusAsync(CancellationToken cancellationToken = default) + { + var request = new SessionAuthGetStatusRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.auth.getStatus", [request], cancellationToken); + } +} + /// Provides session-scoped Model APIs. public sealed class ModelApi { @@ -2947,6 +3374,7 @@ internal McpApi(JsonRpc rpc, string sessionId) { _rpc = rpc; _sessionId = sessionId; + Oauth = new McpOauthApi(rpc, sessionId); } /// Calls "session.mcp.list". @@ -2976,6 +3404,30 @@ public async Task ReloadAsync(CancellationToken cancellationToken = default) var request = new SessionMcpReloadRequest { SessionId = _sessionId }; await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.reload", [request], cancellationToken); } + + /// Oauth APIs. + public McpOauthApi Oauth { get; } +} + +/// Provides session-scoped McpOauth APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class McpOauthApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal McpOauthApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.mcp.oauth.login". + public async Task LoginAsync(string serverName, bool? forceReauth = null, string? clientName = null, string? callbackSuccessMessage = null, CancellationToken cancellationToken = default) + { + var request = new McpOauthLoginRequest { SessionId = _sessionId, ServerName = serverName, ForceReauth = forceReauth, ClientName = clientName, CallbackSuccessMessage = callbackSuccessMessage }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.mcp.oauth.login", [request], cancellationToken); + } } /// Provides session-scoped Plugins APIs. @@ -3126,6 +3578,20 @@ public async Task HandlePendingPermissionRequestAsync(s var request = new PermissionDecisionRequest { SessionId = _sessionId, RequestId = requestId, Result = result }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.permissions.handlePendingPermissionRequest", [request], cancellationToken); } + + /// Calls "session.permissions.setApproveAll". + public async Task SetApproveAllAsync(bool enabled, CancellationToken cancellationToken = default) + { + var request = new PermissionsSetApproveAllRequest { SessionId = _sessionId, Enabled = enabled }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.permissions.setApproveAll", [request], cancellationToken); + } + + /// Calls "session.permissions.resetSessionApprovals". + public async Task ResetSessionApprovalsAsync(CancellationToken cancellationToken = default) + { + var request = new PermissionsResetSessionApprovalsRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.permissions.resetSessionApprovals", [request], cancellationToken); + } } /// Provides session-scoped Shell APIs. @@ -3353,6 +3819,7 @@ public static void RegisterClientSessionApiHandlers(JsonRpc rpc, FuncAuto mode switch request notification requiring user approval. +/// Represents the auto_mode_switch.requested event. +public partial class AutoModeSwitchRequestedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "auto_mode_switch.requested"; + + /// The auto_mode_switch.requested event payload. + [JsonPropertyName("data")] + public required AutoModeSwitchRequestedData Data { get; set; } +} + +/// Auto mode switch completion notification. +/// Represents the auto_mode_switch.completed event. +public partial class AutoModeSwitchCompletedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "auto_mode_switch.completed"; + + /// The auto_mode_switch.completed event payload. + [JsonPropertyName("data")] + public required AutoModeSwitchCompletedData Data { get; set; } +} + /// SDK command registration change notification. /// Represents the commands.changed event. public partial class CommandsChangedEvent : SessionEvent @@ -1580,7 +1608,7 @@ public partial class SessionCompactionCompleteData [JsonPropertyName("checkpointPath")] public string? CheckpointPath { get; set; } - /// Token usage breakdown for the compaction LLM call. + /// Token usage breakdown for the compaction LLM call (aligned with assistant.usage format). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("compactionTokensUsed")] public CompactionCompleteCompactionTokensUsed? CompactionTokensUsed { get; set; } @@ -2558,6 +2586,31 @@ public partial class CommandCompletedData public required string RequestId { get; set; } } +/// Auto mode switch request notification requiring user approval. +public partial class AutoModeSwitchRequestedData +{ + /// The rate limit error code that triggered this request. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("errorCode")] + public string? ErrorCode { get; set; } + + /// Unique identifier for this request; used to respond via session.respondToAutoModeSwitch(). + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } +} + +/// Auto mode switch completion notification. +public partial class AutoModeSwitchCompletedData +{ + /// Request ID of the resolved request; clients should dismiss any UI for this request. + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } + + /// The user's choice: 'yes', 'yes_always', or 'no'. + [JsonPropertyName("response")] + public required string Response { get; set; } +} + /// SDK command registration change notification. public partial class CommandsChangedData { @@ -2822,21 +2875,78 @@ public partial class ShutdownModelMetric public required ShutdownModelMetricUsage Usage { get; set; } } -/// Token usage breakdown for the compaction LLM call. +/// Token usage detail for a single billing category. +/// Nested data type for CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail. +public partial class CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail +{ + /// Number of tokens in this billing batch. + [JsonPropertyName("batchSize")] + public required double BatchSize { get; set; } + + /// Cost per batch of tokens. + [JsonPropertyName("costPerBatch")] + public required double CostPerBatch { get; set; } + + /// Total token count for this entry. + [JsonPropertyName("tokenCount")] + public required double TokenCount { get; set; } + + /// Token category (e.g., "input", "output"). + [JsonPropertyName("tokenType")] + public required string TokenType { get; set; } +} + +/// Per-request cost and usage data from the CAPI copilot_usage response field. +/// Nested data type for CompactionCompleteCompactionTokensUsedCopilotUsage. +public partial class CompactionCompleteCompactionTokensUsedCopilotUsage +{ + /// Itemized token usage breakdown. + [JsonPropertyName("tokenDetails")] + public required CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail[] TokenDetails { get; set; } + + /// Total cost in nano-AIU (AI Units) for this request. + [JsonPropertyName("totalNanoAiu")] + public required double TotalNanoAiu { get; set; } +} + +/// Token usage breakdown for the compaction LLM call (aligned with assistant.usage format). /// Nested data type for CompactionCompleteCompactionTokensUsed. public partial class CompactionCompleteCompactionTokensUsed { /// Cached input tokens reused in the compaction LLM call. - [JsonPropertyName("cachedInput")] - public required double CachedInput { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cacheReadTokens")] + public double? CacheReadTokens { get; set; } + + /// Tokens written to prompt cache in the compaction LLM call. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("cacheWriteTokens")] + public double? CacheWriteTokens { get; set; } + + /// Per-request cost and usage data from the CAPI copilot_usage response field. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("copilotUsage")] + public CompactionCompleteCompactionTokensUsedCopilotUsage? CopilotUsage { get; set; } + + /// Duration of the compaction LLM call in milliseconds. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("duration")] + public double? Duration { get; set; } /// Input tokens consumed by the compaction LLM call. - [JsonPropertyName("input")] - public required double Input { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("inputTokens")] + public double? InputTokens { get; set; } + + /// Model identifier used for the compaction LLM call. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("model")] + public string? Model { get; set; } /// Output tokens produced by the compaction LLM call. - [JsonPropertyName("output")] - public required double Output { get; set; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("outputTokens")] + public double? OutputTokens { get; set; } } /// Optional line range to scope the attachment to a specific section of the file. @@ -4145,6 +4255,12 @@ public enum PermissionCompletedKind /// The approved variant. [JsonStringEnumMemberName("approved")] Approved, + /// The approved-for-session variant. + [JsonStringEnumMemberName("approved-for-session")] + ApprovedForSession, + /// The approved-for-location variant. + [JsonStringEnumMemberName("approved-for-location")] + ApprovedForLocation, /// The denied-by-rules variant. [JsonStringEnumMemberName("denied-by-rules")] DeniedByRules, @@ -4296,6 +4412,10 @@ public enum ExtensionsLoadedExtensionStatus [JsonSerializable(typeof(AssistantUsageData))] [JsonSerializable(typeof(AssistantUsageEvent))] [JsonSerializable(typeof(AssistantUsageQuotaSnapshot))] +[JsonSerializable(typeof(AutoModeSwitchCompletedData))] +[JsonSerializable(typeof(AutoModeSwitchCompletedEvent))] +[JsonSerializable(typeof(AutoModeSwitchRequestedData))] +[JsonSerializable(typeof(AutoModeSwitchRequestedEvent))] [JsonSerializable(typeof(CapabilitiesChangedData))] [JsonSerializable(typeof(CapabilitiesChangedEvent))] [JsonSerializable(typeof(CapabilitiesChangedUI))] @@ -4309,6 +4429,8 @@ public enum ExtensionsLoadedExtensionStatus [JsonSerializable(typeof(CommandsChangedData))] [JsonSerializable(typeof(CommandsChangedEvent))] [JsonSerializable(typeof(CompactionCompleteCompactionTokensUsed))] +[JsonSerializable(typeof(CompactionCompleteCompactionTokensUsedCopilotUsage))] +[JsonSerializable(typeof(CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail))] [JsonSerializable(typeof(CustomAgentsUpdatedAgent))] [JsonSerializable(typeof(ElicitationCompletedData))] [JsonSerializable(typeof(ElicitationCompletedEvent))] diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index e42c34f5d..954b506d7 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -1745,6 +1745,7 @@ protected SessionConfig(SessionConfig? other) Provider = other.Provider; ReasoningEffort = other.ReasoningEffort; CreateSessionFsHandler = other.CreateSessionFsHandler; + GitHubToken = other.GitHubToken; SessionId = other.SessionId; SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null; Streaming = other.Streaming; @@ -1934,6 +1935,13 @@ protected SessionConfig(SessionConfig? other) /// public Func? CreateSessionFsHandler { get; set; } + /// + /// GitHub token for per-session authentication. + /// When provided, the runtime resolves this token into a full GitHub identity + /// and stores it on the session for content exclusion, model routing, and quota checks. + /// + public string? GitHubToken { get; set; } + /// /// Creates a shallow clone of this instance. /// @@ -1995,6 +2003,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) Provider = other.Provider; ReasoningEffort = other.ReasoningEffort; CreateSessionFsHandler = other.CreateSessionFsHandler; + GitHubToken = other.GitHubToken; SkillDirectories = other.SkillDirectories is not null ? [.. other.SkillDirectories] : null; Streaming = other.Streaming; IncludeSubAgentStreamingEvents = other.IncludeSubAgentStreamingEvents; @@ -2181,6 +2190,13 @@ protected ResumeSessionConfig(ResumeSessionConfig? other) /// public Func? CreateSessionFsHandler { get; set; } + /// + /// GitHub token for per-session authentication. + /// When provided, the runtime resolves this token into a full GitHub identity + /// and stores it on the session for content exclusion, model routing, and quota checks. + /// + public string? GitHubToken { get; set; } + /// /// Creates a shallow clone of this instance. /// diff --git a/dotnet/test/Harness/CapiProxy.cs b/dotnet/test/Harness/CapiProxy.cs index 1c775adb0..8b167972e 100644 --- a/dotnet/test/Harness/CapiProxy.cs +++ b/dotnet/test/Harness/CapiProxy.cs @@ -132,6 +132,16 @@ public async Task> GetExchangesAsync() ?? []; } + public async Task SetCopilotUserByTokenAsync(string token, CopilotUserConfig response) + { + var url = await (_startupTask ?? throw new InvalidOperationException("Proxy not started")); + + using var client = new HttpClient(); + var payload = new CopilotUserByTokenRequest(token, response); + var resp = await client.PostAsJsonAsync($"{url}/copilot-user-config", payload, CapiProxyJsonContext.Default.CopilotUserByTokenRequest); + resp.EnsureSuccessStatusCode(); + } + public async ValueTask DisposeAsync() { await StopAsync(); @@ -152,9 +162,20 @@ private static string FindRepoRoot() [JsonSourceGenerationOptions(JsonSerializerDefaults.Web)] [JsonSerializable(typeof(ConfigureRequest))] [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(CopilotUserByTokenRequest))] private partial class CapiProxyJsonContext : JsonSerializerContext; } +public record CopilotUserByTokenRequest(string Token, CopilotUserConfig Response); + +public record CopilotUserConfig( + string Login, + string CopilotPlan, + CopilotUserEndpoints Endpoints, + string AnalyticsTrackingId); + +public record CopilotUserEndpoints(string Api, string Telemetry); + public record ParsedHttpExchange(ChatCompletionRequest Request, ChatCompletionResponse? Response); public record ChatCompletionRequest( diff --git a/dotnet/test/Harness/E2ETestContext.cs b/dotnet/test/Harness/E2ETestContext.cs index 7b47ab0b7..1d5cf8839 100644 --- a/dotnet/test/Harness/E2ETestContext.cs +++ b/dotnet/test/Harness/E2ETestContext.cs @@ -83,6 +83,11 @@ public Task> GetExchangesAsync() return _proxy.GetExchangesAsync(); } + public Task SetCopilotUserByTokenAsync(string token, CopilotUserConfig response) + { + return _proxy.SetCopilotUserByTokenAsync(token, response); + } + public IReadOnlyDictionary GetEnvironment() { var env = Environment.GetEnvironmentVariables() diff --git a/dotnet/test/PerSessionAuthTests.cs b/dotnet/test/PerSessionAuthTests.cs new file mode 100644 index 000000000..b707a7546 --- /dev/null +++ b/dotnet/test/PerSessionAuthTests.cs @@ -0,0 +1,123 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.SDK.Test.Harness; +using Xunit; +using Xunit.Abstractions; + +namespace GitHub.Copilot.SDK.Test; + +public class PerSessionAuthTests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "per-session-auth", output) +{ + /// + /// Creates a client with COPILOT_DEBUG_GITHUB_API_URL redirected to the proxy + /// so per-session auth token resolution (fetchCopilotUser) is intercepted. + /// + private CopilotClient CreateAuthTestClient() + { + var env = new Dictionary(Ctx.GetEnvironment()) + { + ["COPILOT_DEBUG_GITHUB_API_URL"] = Ctx.ProxyUrl, + }; + return Ctx.CreateClient(options: new CopilotClientOptions { Environment = env }); + } + + private async Task SetupCopilotUsersAsync() + { + await Ctx.SetCopilotUserByTokenAsync("token-alice", new CopilotUserConfig( + Login: "alice", + CopilotPlan: "individual_pro", + Endpoints: new CopilotUserEndpoints(Api: Ctx.ProxyUrl, Telemetry: "https://localhost:1/telemetry"), + AnalyticsTrackingId: "alice-tracking-id" + )); + + await Ctx.SetCopilotUserByTokenAsync("token-bob", new CopilotUserConfig( + Login: "bob", + CopilotPlan: "business", + Endpoints: new CopilotUserEndpoints(Api: Ctx.ProxyUrl, Telemetry: "https://localhost:1/telemetry"), + AnalyticsTrackingId: "bob-tracking-id" + )); + } + + private CopilotClient? _authClient; + + private CopilotClient AuthClient => _authClient ??= CreateAuthTestClient(); + + [Fact] + public async Task ShouldAuthenticateWithGitHubToken() + { + await SetupCopilotUsersAsync(); + + await using var session = await AuthClient.CreateSessionAsync(new SessionConfig + { + GitHubToken = "token-alice", + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + var authStatus = await session.Rpc.Auth.GetStatusAsync(); + + Assert.True(authStatus.IsAuthenticated); + Assert.Equal("alice", authStatus.Login); + } + + [Fact] + public async Task ShouldIsolateAuthBetweenSessions() + { + await SetupCopilotUsersAsync(); + + await using var sessionA = await AuthClient.CreateSessionAsync(new SessionConfig + { + GitHubToken = "token-alice", + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + await using var sessionB = await AuthClient.CreateSessionAsync(new SessionConfig + { + GitHubToken = "token-bob", + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + var statusA = await sessionA.Rpc.Auth.GetStatusAsync(); + var statusB = await sessionB.Rpc.Auth.GetStatusAsync(); + + Assert.True(statusA.IsAuthenticated); + Assert.Equal("alice", statusA.Login); + + Assert.True(statusB.IsAuthenticated); + Assert.Equal("bob", statusB.Login); + } + + [Fact] + public async Task ShouldBeUnauthenticatedWithoutToken() + { + await using var session = await AuthClient.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + + var authStatus = await session.Rpc.Auth.GetStatusAsync(); + + // Without a per-session token, there is no per-session identity. + // In CI the process-level fake token may still authenticate globally, + // so we check Login rather than IsAuthenticated. + Assert.Null(authStatus.Login); + } + + [Fact] + public async Task ShouldFailWithInvalidToken() + { + await SetupCopilotUsersAsync(); + + var ex = await Assert.ThrowsAnyAsync(async () => + { + await using var session = await AuthClient.CreateSessionAsync(new SessionConfig + { + GitHubToken = "invalid-token", + OnPermissionRequest = PermissionHandler.ApproveAll, + }); + }); + + Assert.NotNull(ex); + } +} diff --git a/go/client.go b/go/client.go index 4eb56e639..f0ad86554 100644 --- a/go/client.go +++ b/go/client.go @@ -597,6 +597,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses req.SkillDirectories = config.SkillDirectories req.DisabledSkills = config.DisabledSkills req.InfiniteSessions = config.InfiniteSessions + req.GitHubToken = config.GitHubToken if len(config.Commands) > 0 { cmds := make([]wireCommand, 0, len(config.Commands)) @@ -782,6 +783,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, req.SkillDirectories = config.SkillDirectories req.DisabledSkills = config.DisabledSkills req.InfiniteSessions = config.InfiniteSessions + req.GitHubToken = config.GitHubToken req.RequestPermission = Bool(true) if len(config.Commands) > 0 { diff --git a/go/generated_session_events.go b/go/generated_session_events.go index d0bbde414..a85c7955f 100644 --- a/go/generated_session_events.go +++ b/go/generated_session_events.go @@ -449,6 +449,18 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeAutoModeSwitchRequested: + var d AutoModeSwitchRequestedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeAutoModeSwitchCompleted: + var d AutoModeSwitchCompletedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeCommandsChanged: var d CommandsChangedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -607,6 +619,8 @@ const ( SessionEventTypeCommandQueued SessionEventType = "command.queued" SessionEventTypeCommandExecute SessionEventType = "command.execute" SessionEventTypeCommandCompleted SessionEventType = "command.completed" + SessionEventTypeAutoModeSwitchRequested SessionEventType = "auto_mode_switch.requested" + SessionEventTypeAutoModeSwitchCompleted SessionEventType = "auto_mode_switch.completed" SessionEventTypeCommandsChanged SessionEventType = "commands.changed" SessionEventTypeCapabilitiesChanged SessionEventType = "capabilities.changed" SessionEventTypeExitPlanModeRequested SessionEventType = "exit_plan_mode.requested" @@ -677,6 +691,26 @@ type AssistantMessageData struct { func (*AssistantMessageData) sessionEventData() {} +// Auto mode switch completion notification +type AutoModeSwitchCompletedData struct { + // Request ID of the resolved request; clients should dismiss any UI for this request + RequestID string `json:"requestId"` + // The user's choice: 'yes', 'yes_always', or 'no' + Response string `json:"response"` +} + +func (*AutoModeSwitchCompletedData) sessionEventData() {} + +// Auto mode switch request notification requiring user approval +type AutoModeSwitchRequestedData struct { + // The rate limit error code that triggered this request + ErrorCode *string `json:"errorCode,omitempty"` + // Unique identifier for this request; used to respond via session.respondToAutoModeSwitch() + RequestID string `json:"requestId"` +} + +func (*AutoModeSwitchRequestedData) sessionEventData() {} + // Context window breakdown at the start of LLM-powered conversation compaction type SessionCompactionStartData struct { // Token count from non-system messages (user, assistant, tool) at compaction start @@ -695,7 +729,7 @@ type SessionCompactionCompleteData struct { CheckpointNumber *float64 `json:"checkpointNumber,omitempty"` // File path where the checkpoint was stored CheckpointPath *string `json:"checkpointPath,omitempty"` - // Token usage breakdown for the compaction LLM call + // Token usage breakdown for the compaction LLM call (aligned with assistant.usage format) CompactionTokensUsed *CompactionCompleteCompactionTokensUsed `json:"compactionTokensUsed,omitempty"` // Token count from non-system messages (user, assistant, tool) after compaction ConversationTokens *float64 `json:"conversationTokens,omitempty"` @@ -1864,6 +1898,14 @@ type AssistantUsageCopilotUsage struct { TotalNanoAiu float64 `json:"totalNanoAiu"` } +// Per-request cost and usage data from the CAPI copilot_usage response field +type CompactionCompleteCompactionTokensUsedCopilotUsage struct { + // Itemized token usage breakdown + TokenDetails []CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail `json:"tokenDetails"` + // Total cost in nano-AIU (AI Units) for this request + TotalNanoAiu float64 `json:"totalNanoAiu"` +} + // Position range of the selection within the file type UserMessageAttachmentSelectionDetails struct { // End position of the selection @@ -1954,14 +1996,22 @@ type ShutdownModelMetricUsage struct { ReasoningTokens *float64 `json:"reasoningTokens,omitempty"` } -// Token usage breakdown for the compaction LLM call +// Token usage breakdown for the compaction LLM call (aligned with assistant.usage format) type CompactionCompleteCompactionTokensUsed struct { // Cached input tokens reused in the compaction LLM call - CachedInput float64 `json:"cachedInput"` + CacheReadTokens *float64 `json:"cacheReadTokens,omitempty"` + // Tokens written to prompt cache in the compaction LLM call + CacheWriteTokens *float64 `json:"cacheWriteTokens,omitempty"` + // Per-request cost and usage data from the CAPI copilot_usage response field + CopilotUsage *CompactionCompleteCompactionTokensUsedCopilotUsage `json:"copilotUsage,omitempty"` + // Duration of the compaction LLM call in milliseconds + Duration *float64 `json:"duration,omitempty"` // Input tokens consumed by the compaction LLM call - Input float64 `json:"input"` + InputTokens *float64 `json:"inputTokens,omitempty"` + // Model identifier used for the compaction LLM call + Model *string `json:"model,omitempty"` // Output tokens produced by the compaction LLM call - Output float64 `json:"output"` + OutputTokens *float64 `json:"outputTokens,omitempty"` } // Token usage detail for a single billing category @@ -1976,6 +2026,18 @@ type AssistantUsageCopilotUsageTokenDetail struct { TokenType string `json:"tokenType"` } +// Token usage detail for a single billing category +type CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail struct { + // Number of tokens in this billing batch + BatchSize float64 `json:"batchSize"` + // Cost per batch of tokens + CostPerBatch float64 `json:"costPerBatch"` + // Total token count for this entry + TokenCount float64 `json:"tokenCount"` + // Token category (e.g., "input", "output") + TokenType string `json:"tokenType"` +} + // Tool execution result on success type ToolExecutionCompleteResult struct { // Concise tool result text sent to the LLM for chat completion, potentially truncated for token efficiency @@ -2214,6 +2276,8 @@ type PermissionCompletedKind string const ( PermissionCompletedKindApproved PermissionCompletedKind = "approved" + PermissionCompletedKindApprovedForSession PermissionCompletedKind = "approved-for-session" + PermissionCompletedKindApprovedForLocation PermissionCompletedKind = "approved-for-location" PermissionCompletedKindDeniedByRules PermissionCompletedKind = "denied-by-rules" PermissionCompletedKindDeniedNoApprovalRuleAndCouldNotRequestFromUser PermissionCompletedKind = "denied-no-approval-rule-and-could-not-request-from-user" PermissionCompletedKindDeniedInteractivelyByUser PermissionCompletedKind = "denied-interactively-by-user" diff --git a/go/internal/e2e/per_session_auth_test.go b/go/internal/e2e/per_session_auth_test.go new file mode 100644 index 000000000..8d773c559 --- /dev/null +++ b/go/internal/e2e/per_session_auth_test.go @@ -0,0 +1,134 @@ +package e2e + +import ( + "testing" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" +) + +func TestPerSessionAuth(t *testing.T) { + ctx := testharness.NewTestContext(t) + + // Create client with COPILOT_DEBUG_GITHUB_API_URL redirected to the proxy + // so per-session auth token resolution (fetchCopilotUser) is intercepted. + client := ctx.NewClient(func(opts *copilot.ClientOptions) { + opts.Env = append(opts.Env, "COPILOT_DEBUG_GITHUB_API_URL="+ctx.ProxyURL) + }) + t.Cleanup(func() { client.ForceStop() }) + // Register per-token user configs on the proxy + if err := ctx.SetCopilotUserByToken("token-alice", map[string]interface{}{ + "login": "alice", + "copilot_plan": "individual_pro", + "endpoints": map[string]interface{}{"api": ctx.ProxyURL, "telemetry": "https://localhost:1/telemetry"}, + "analytics_tracking_id": "alice-tracking-id", + }); err != nil { + t.Fatalf("Failed to set copilot user for alice: %v", err) + } + + if err := ctx.SetCopilotUserByToken("token-bob", map[string]interface{}{ + "login": "bob", + "copilot_plan": "business", + "endpoints": map[string]interface{}{"api": ctx.ProxyURL, "telemetry": "https://localhost:1/telemetry"}, + "analytics_tracking_id": "bob-tracking-id", + }); err != nil { + t.Fatalf("Failed to set copilot user for bob: %v", err) + } + + t.Run("should authenticate with per-session token", func(t *testing.T) { + ctx.ConfigureForTest(t) + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + GitHubToken: "token-alice", + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + authStatus, err := session.RPC.Auth.GetStatus(t.Context()) + if err != nil { + t.Fatalf("Failed to get auth status: %v", err) + } + + if !authStatus.IsAuthenticated { + t.Errorf("Expected session to be authenticated") + } + if authStatus.Login == nil || *authStatus.Login != "alice" { + t.Errorf("Expected login to be 'alice', got %v", authStatus.Login) + } + }) + + t.Run("should isolate auth between sessions", func(t *testing.T) { + ctx.ConfigureForTest(t) + + sessionA, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + GitHubToken: "token-alice", + }) + if err != nil { + t.Fatalf("Failed to create session A: %v", err) + } + + sessionB, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + GitHubToken: "token-bob", + }) + if err != nil { + t.Fatalf("Failed to create session B: %v", err) + } + + statusA, err := sessionA.RPC.Auth.GetStatus(t.Context()) + if err != nil { + t.Fatalf("Failed to get auth status for session A: %v", err) + } + + statusB, err := sessionB.RPC.Auth.GetStatus(t.Context()) + if err != nil { + t.Fatalf("Failed to get auth status for session B: %v", err) + } + + if statusA.Login == nil || *statusA.Login != "alice" { + t.Errorf("Expected session A login to be 'alice', got %v", statusA.Login) + } + if statusB.Login == nil || *statusB.Login != "bob" { + t.Errorf("Expected session B login to be 'bob', got %v", statusB.Login) + } + }) + + t.Run("should be unauthenticated without token", func(t *testing.T) { + ctx.ConfigureForTest(t) + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + authStatus, err := session.RPC.Auth.GetStatus(t.Context()) + if err != nil { + t.Fatalf("Failed to get auth status: %v", err) + } + + // Without a per-session token, there is no per-session identity. + // In CI the process-level fake token may still authenticate globally, + // so we check Login rather than IsAuthenticated. + if authStatus.Login != nil && *authStatus.Login != "" { + t.Errorf("Expected no per-session login without token, got %q", *authStatus.Login) + } + }) + + t.Run("should fail with invalid token", func(t *testing.T) { + ctx.ConfigureForTest(t) + + _, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + GitHubToken: "invalid-token", + }) + if err == nil { + t.Fatal("Expected session creation to fail with invalid token") + } + t.Logf("Got expected error: %v", err) + }) +} diff --git a/go/internal/e2e/rpc_test.go b/go/internal/e2e/rpc_test.go index 819e8ccca..3ca20d43b 100644 --- a/go/internal/e2e/rpc_test.go +++ b/go/internal/e2e/rpc_test.go @@ -64,7 +64,7 @@ func TestRpc(t *testing.T) { t.Skip("Not authenticated - skipping models.list test") } - result, err := client.RPC.Models.List(t.Context()) + result, err := client.RPC.Models.List(t.Context(), nil) if err != nil { t.Fatalf("Failed to call RPC.Models.List: %v", err) } @@ -101,7 +101,7 @@ func TestRpc(t *testing.T) { t.Skip("Not authenticated - skipping account.getQuota test") } - result, err := client.RPC.Account.GetQuota(t.Context()) + result, err := client.RPC.Account.GetQuota(t.Context(), nil) if err != nil { t.Fatalf("Failed to call RPC.Account.GetQuota: %v", err) } diff --git a/go/internal/e2e/testharness/context.go b/go/internal/e2e/testharness/context.go index 269b53789..9f73c2267 100644 --- a/go/internal/e2e/testharness/context.go +++ b/go/internal/e2e/testharness/context.go @@ -144,6 +144,11 @@ func (c *TestContext) GetExchanges() ([]ParsedHttpExchange, error) { return c.proxy.GetExchanges() } +// SetCopilotUserByToken registers a per-token user configuration on the proxy. +func (c *TestContext) SetCopilotUserByToken(token string, response map[string]interface{}) error { + return c.proxy.SetCopilotUserByToken(token, response) +} + // Env returns environment variables configured for isolated testing. func (c *TestContext) Env() []string { env := os.Environ() diff --git a/go/internal/e2e/testharness/proxy.go b/go/internal/e2e/testharness/proxy.go index 0caf19403..887f7134d 100644 --- a/go/internal/e2e/testharness/proxy.go +++ b/go/internal/e2e/testharness/proxy.go @@ -2,6 +2,7 @@ package testharness import ( "bufio" + "bytes" "encoding/json" "fmt" "io" @@ -251,3 +252,32 @@ func (p *CapiProxy) URL() string { defer p.mu.Unlock() return p.proxyURL } + +// SetCopilotUserByToken registers a per-token user configuration on the proxy. +func (p *CapiProxy) SetCopilotUserByToken(token string, response map[string]interface{}) error { + p.mu.Lock() + url := p.proxyURL + p.mu.Unlock() + + if url == "" { + return fmt.Errorf("proxy not started") + } + + body := map[string]interface{}{ + "token": token, + "response": response, + } + data, err := json.Marshal(body) + if err != nil { + return err + } + resp, err := http.Post(url+"/copilot-user-config", "application/json", bytes.NewReader(data)) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + return fmt.Errorf("setCopilotUserByToken: unexpected status %d", resp.StatusCode) + } + return nil +} diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index 683fb2a5c..0e764b9a4 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -13,6 +13,7 @@ import ( ) type RPCTypes struct { + AccountGetQuotaRequest AccountGetQuotaRequest `json:"AccountGetQuotaRequest"` AccountGetQuotaResult AccountGetQuotaResult `json:"AccountGetQuotaResult"` AccountQuotaSnapshot AccountQuotaSnapshot `json:"AccountQuotaSnapshot"` AgentDeselectResult AgentDeselectResult `json:"AgentDeselectResult"` @@ -22,6 +23,7 @@ type RPCTypes struct { AgentReloadResult AgentReloadResult `json:"AgentReloadResult"` AgentSelectRequest AgentSelectRequest `json:"AgentSelectRequest"` AgentSelectResult AgentSelectResult `json:"AgentSelectResult"` + AuthInfoType AuthInfoType `json:"AuthInfoType"` CommandsHandlePendingCommandRequest CommandsHandlePendingCommandRequest `json:"CommandsHandlePendingCommandRequest"` CommandsHandlePendingCommandResult CommandsHandlePendingCommandResult `json:"CommandsHandlePendingCommandResult"` CurrentModel CurrentModel `json:"CurrentModel"` @@ -55,6 +57,10 @@ type RPCTypes struct { LogResult LogResult `json:"LogResult"` MCPConfigAddRequest MCPConfigAddRequest `json:"McpConfigAddRequest"` MCPConfigAddResult MCPConfigAddResult `json:"McpConfigAddResult"` + MCPConfigDisableRequest MCPConfigDisableRequest `json:"McpConfigDisableRequest"` + MCPConfigDisableResult MCPConfigDisableResult `json:"McpConfigDisableResult"` + MCPConfigEnableRequest MCPConfigEnableRequest `json:"McpConfigEnableRequest"` + MCPConfigEnableResult MCPConfigEnableResult `json:"McpConfigEnableResult"` MCPConfigList MCPConfigList `json:"McpConfigList"` MCPConfigRemoveRequest MCPConfigRemoveRequest `json:"McpConfigRemoveRequest"` MCPConfigRemoveResult MCPConfigRemoveResult `json:"McpConfigRemoveResult"` @@ -66,6 +72,8 @@ type RPCTypes struct { MCPDiscoverResult MCPDiscoverResult `json:"McpDiscoverResult"` MCPEnableRequest MCPEnableRequest `json:"McpEnableRequest"` MCPEnableResult MCPEnableResult `json:"McpEnableResult"` + MCPOauthLoginRequest MCPOauthLoginRequest `json:"McpOauthLoginRequest"` + MCPOauthLoginResult MCPOauthLoginResult `json:"McpOauthLoginResult"` MCPReloadResult MCPReloadResult `json:"McpReloadResult"` MCPServer MCPServer `json:"McpServer"` MCPServerConfig MCPServerConfig `json:"McpServerConfig"` @@ -88,6 +96,7 @@ type RPCTypes struct { ModelCapabilitiesSupports ModelCapabilitiesSupports `json:"ModelCapabilitiesSupports"` ModelList ModelList `json:"ModelList"` ModelPolicy ModelPolicy `json:"ModelPolicy"` + ModelsListRequest ModelsListRequest `json:"ModelsListRequest"` ModelSwitchToRequest ModelSwitchToRequest `json:"ModelSwitchToRequest"` ModelSwitchToResult ModelSwitchToResult `json:"ModelSwitchToResult"` ModeSetRequest ModeSetRequest `json:"ModeSetRequest"` @@ -97,6 +106,22 @@ type RPCTypes struct { NameSetResult NameSetResult `json:"NameSetResult"` PermissionDecision PermissionDecision `json:"PermissionDecision"` PermissionDecisionApproved PermissionDecisionApproved `json:"PermissionDecisionApproved"` + PermissionDecisionApprovedForLocation PermissionDecisionApprovedForLocation `json:"PermissionDecisionApprovedForLocation"` + PermissionDecisionApprovedForLocationApproval PermissionDecisionApprovedForLocationApproval `json:"PermissionDecisionApprovedForLocationApproval"` + PermissionDecisionApprovedForLocationApprovalCommands PermissionDecisionApprovedForLocationApprovalCommands `json:"PermissionDecisionApprovedForLocationApprovalCommands"` + PermissionDecisionApprovedForLocationApprovalCustomTool PermissionDecisionApprovedForLocationApprovalCustomTool `json:"PermissionDecisionApprovedForLocationApprovalCustomTool"` + PermissionDecisionApprovedForLocationApprovalMCP PermissionDecisionApprovedForLocationApprovalMCP `json:"PermissionDecisionApprovedForLocationApprovalMcp"` + PermissionDecisionApprovedForLocationApprovalMCPSampling PermissionDecisionApprovedForLocationApprovalMCPSampling `json:"PermissionDecisionApprovedForLocationApprovalMcpSampling"` + PermissionDecisionApprovedForLocationApprovalMemory PermissionDecisionApprovedForLocationApprovalMemory `json:"PermissionDecisionApprovedForLocationApprovalMemory"` + PermissionDecisionApprovedForLocationApprovalWrite PermissionDecisionApprovedForLocationApprovalWrite `json:"PermissionDecisionApprovedForLocationApprovalWrite"` + PermissionDecisionApprovedForSession PermissionDecisionApprovedForSession `json:"PermissionDecisionApprovedForSession"` + PermissionDecisionApprovedForSessionApproval PermissionDecisionApprovedForSessionApproval `json:"PermissionDecisionApprovedForSessionApproval"` + PermissionDecisionApprovedForSessionApprovalCommands PermissionDecisionApprovedForSessionApprovalCommands `json:"PermissionDecisionApprovedForSessionApprovalCommands"` + PermissionDecisionApprovedForSessionApprovalCustomTool PermissionDecisionApprovedForSessionApprovalCustomTool `json:"PermissionDecisionApprovedForSessionApprovalCustomTool"` + PermissionDecisionApprovedForSessionApprovalMCP PermissionDecisionApprovedForSessionApprovalMCP `json:"PermissionDecisionApprovedForSessionApprovalMcp"` + PermissionDecisionApprovedForSessionApprovalMCPSampling PermissionDecisionApprovedForSessionApprovalMCPSampling `json:"PermissionDecisionApprovedForSessionApprovalMcpSampling"` + PermissionDecisionApprovedForSessionApprovalMemory PermissionDecisionApprovedForSessionApprovalMemory `json:"PermissionDecisionApprovedForSessionApprovalMemory"` + PermissionDecisionApprovedForSessionApprovalWrite PermissionDecisionApprovedForSessionApprovalWrite `json:"PermissionDecisionApprovedForSessionApprovalWrite"` PermissionDecisionDeniedByContentExclusionPolicy PermissionDecisionDeniedByContentExclusionPolicy `json:"PermissionDecisionDeniedByContentExclusionPolicy"` PermissionDecisionDeniedByPermissionRequestHook PermissionDecisionDeniedByPermissionRequestHook `json:"PermissionDecisionDeniedByPermissionRequestHook"` PermissionDecisionDeniedByRules PermissionDecisionDeniedByRules `json:"PermissionDecisionDeniedByRules"` @@ -104,6 +129,10 @@ type RPCTypes struct { PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser `json:"PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser"` PermissionDecisionRequest PermissionDecisionRequest `json:"PermissionDecisionRequest"` PermissionRequestResult PermissionRequestResult `json:"PermissionRequestResult"` + PermissionsResetSessionApprovalsRequest PermissionsResetSessionApprovalsRequest `json:"PermissionsResetSessionApprovalsRequest"` + PermissionsResetSessionApprovalsResult PermissionsResetSessionApprovalsResult `json:"PermissionsResetSessionApprovalsResult"` + PermissionsSetApproveAllRequest PermissionsSetApproveAllRequest `json:"PermissionsSetApproveAllRequest"` + PermissionsSetApproveAllResult PermissionsSetApproveAllResult `json:"PermissionsSetApproveAllResult"` PingRequest PingRequest `json:"PingRequest"` PingResult PingResult `json:"PingResult"` PlanDeleteResult PlanDeleteResult `json:"PlanDeleteResult"` @@ -114,6 +143,7 @@ type RPCTypes struct { PluginList PluginList `json:"PluginList"` ServerSkill ServerSkill `json:"ServerSkill"` ServerSkillList ServerSkillList `json:"ServerSkillList"` + SessionAuthStatus SessionAuthStatus `json:"SessionAuthStatus"` SessionFSAppendFileRequest SessionFSAppendFileRequest `json:"SessionFsAppendFileRequest"` SessionFSError SessionFSError `json:"SessionFsError"` SessionFSErrorCode SessionFSErrorCode `json:"SessionFsErrorCode"` @@ -196,6 +226,12 @@ type RPCTypes struct { WorkspacesReadFileResult WorkspacesReadFileResult `json:"WorkspacesReadFileResult"` } +type AccountGetQuotaRequest struct { + // GitHub token for per-user quota lookup. When provided, resolves this token to determine + // the user's quota instead of using the global auth. + GitHubToken *string `json:"gitHubToken,omitempty"` +} + type AccountGetQuotaResult struct { // Quota snapshots keyed by type (e.g., chat, completions, premium_interactions) QuotaSnapshots map[string]AccountQuotaSnapshot `json:"quotaSnapshots"` @@ -463,6 +499,25 @@ type MCPServerConfig struct { type MCPConfigAddResult struct { } +type MCPConfigDisableRequest struct { + // Names of MCP servers to disable. Each server is added to the persisted disabled list so + // new sessions skip it. Already-disabled names are ignored. Active sessions keep their + // current connections until they end. + Names []string `json:"names"` +} + +type MCPConfigDisableResult struct { +} + +type MCPConfigEnableRequest struct { + // Names of MCP servers to enable. Each server is removed from the persisted disabled list + // so new sessions spawn it. Unknown or already-enabled names are ignored. + Names []string `json:"names"` +} + +type MCPConfigEnableResult struct { +} + type MCPConfigList struct { // All MCP servers from user config, keyed by name Servers map[string]MCPServerConfig `json:"servers"` @@ -512,6 +567,34 @@ type MCPEnableRequest struct { type MCPEnableResult struct { } +type MCPOauthLoginRequest struct { + // Optional override for the body text shown on the OAuth loopback callback success page. + // When omitted, the runtime applies a neutral fallback; callers driving interactive auth + // should pass surface-specific copy telling the user where to return. + CallbackSuccessMessage *string `json:"callbackSuccessMessage,omitempty"` + // Optional override for the OAuth client display name shown on the consent screen. Applies + // to newly registered dynamic clients only — existing registrations keep the name they were + // created with. When omitted, the runtime applies a neutral fallback; callers driving + // interactive auth should pass their own surface-specific label so the consent screen + // matches the product the user sees. + ClientName *string `json:"clientName,omitempty"` + // When true, clears any cached OAuth token for the server and runs a full new + // authorization. Use when the user explicitly wants to switch accounts or believes their + // session is stuck. + ForceReauth *bool `json:"forceReauth,omitempty"` + // Name of the remote MCP server to authenticate + ServerName string `json:"serverName"` +} + +type MCPOauthLoginResult struct { + // URL the caller should open in a browser to complete OAuth. Omitted when cached tokens + // were still valid and no browser interaction was needed — the server is already + // reconnected in that case. When present, the runtime starts the callback listener before + // returning and continues the flow in the background; completion is signaled via + // session.mcp_server_status_changed. + AuthorizationURL *string `json:"authorizationUrl,omitempty"` +} + type MCPReloadResult struct { } @@ -634,7 +717,7 @@ type ModelPolicy struct { // Current policy state for this model State string `json:"state"` // Usage terms or conditions for this model - Terms string `json:"terms"` + Terms *string `json:"terms,omitempty"` } // Override individual model capabilities resolved by the runtime @@ -688,6 +771,12 @@ type ModelSwitchToResult struct { ModelID *string `json:"modelId,omitempty"` } +type ModelsListRequest struct { + // GitHub token for per-user model listing. When provided, resolves this token to determine + // the user's Copilot plan and available models instead of using the global auth. + GitHubToken *string `json:"gitHubToken,omitempty"` +} + type NameGetResult struct { // The session name, falling back to the auto-generated summary, or null if neither exists Name *string `json:"name"` @@ -704,6 +793,10 @@ type NameSetResult struct { type PermissionDecision struct { // The permission request was approved // + // Approved and remembered for the rest of the session + // + // Approved and persisted for this project location + // // Denied because approval rules explicitly blocked it // // Denied because no approval rule matched and user confirmation was unavailable @@ -714,6 +807,12 @@ type PermissionDecision struct { // // Denied by a permission request hook registered by an extension or plugin Kind PermissionDecisionKind `json:"kind"` + // The approval to add as a session-scoped rule + // + // The approval to persist for this location + Approval *PermissionDecisionApprovedForLocationApproval `json:"approval,omitempty"` + // The location key (git root or cwd) to persist the approval to + LocationKey *string `json:"locationKey,omitempty"` // Rules that denied the request Rules []any `json:"rules,omitempty"` // Optional feedback from the user explaining the denial @@ -733,6 +832,96 @@ type PermissionDecisionApproved struct { Kind PermissionDecisionApprovedKind `json:"kind"` } +type PermissionDecisionApprovedForLocation struct { + // The approval to persist for this location + Approval PermissionDecisionApprovedForLocationApproval `json:"approval"` + // Approved and persisted for this project location + Kind PermissionDecisionApprovedForLocationKind `json:"kind"` + // The location key (git root or cwd) to persist the approval to + LocationKey string `json:"locationKey"` +} + +// The approval to persist for this location +type PermissionDecisionApprovedForLocationApproval struct { + CommandIdentifiers []string `json:"commandIdentifiers,omitempty"` + Kind ApprovalKind `json:"kind"` + ServerName *string `json:"serverName,omitempty"` + ToolName *string `json:"toolName"` +} + +type PermissionDecisionApprovedForLocationApprovalCommands struct { + CommandIdentifiers []string `json:"commandIdentifiers"` + Kind PermissionDecisionApprovedForLocationApprovalCommandsKind `json:"kind"` +} + +type PermissionDecisionApprovedForLocationApprovalCustomTool struct { + Kind PermissionDecisionApprovedForLocationApprovalCustomToolKind `json:"kind"` + ToolName string `json:"toolName"` +} + +type PermissionDecisionApprovedForLocationApprovalMCP struct { + Kind PermissionDecisionApprovedForLocationApprovalMCPKind `json:"kind"` + ServerName string `json:"serverName"` + ToolName *string `json:"toolName"` +} + +type PermissionDecisionApprovedForLocationApprovalMCPSampling struct { + Kind PermissionDecisionApprovedForLocationApprovalMCPSamplingKind `json:"kind"` + ServerName string `json:"serverName"` +} + +type PermissionDecisionApprovedForLocationApprovalMemory struct { + Kind PermissionDecisionApprovedForLocationApprovalMemoryKind `json:"kind"` +} + +type PermissionDecisionApprovedForLocationApprovalWrite struct { + Kind PermissionDecisionApprovedForLocationApprovalWriteKind `json:"kind"` +} + +type PermissionDecisionApprovedForSession struct { + // The approval to add as a session-scoped rule + Approval PermissionDecisionApprovedForSessionApproval `json:"approval"` + // Approved and remembered for the rest of the session + Kind PermissionDecisionApprovedForSessionKind `json:"kind"` +} + +// The approval to add as a session-scoped rule +type PermissionDecisionApprovedForSessionApproval struct { + CommandIdentifiers []string `json:"commandIdentifiers,omitempty"` + Kind ApprovalKind `json:"kind"` + ServerName *string `json:"serverName,omitempty"` + ToolName *string `json:"toolName"` +} + +type PermissionDecisionApprovedForSessionApprovalCommands struct { + CommandIdentifiers []string `json:"commandIdentifiers"` + Kind PermissionDecisionApprovedForLocationApprovalCommandsKind `json:"kind"` +} + +type PermissionDecisionApprovedForSessionApprovalCustomTool struct { + Kind PermissionDecisionApprovedForLocationApprovalCustomToolKind `json:"kind"` + ToolName string `json:"toolName"` +} + +type PermissionDecisionApprovedForSessionApprovalMCP struct { + Kind PermissionDecisionApprovedForLocationApprovalMCPKind `json:"kind"` + ServerName string `json:"serverName"` + ToolName *string `json:"toolName"` +} + +type PermissionDecisionApprovedForSessionApprovalMCPSampling struct { + Kind PermissionDecisionApprovedForLocationApprovalMCPSamplingKind `json:"kind"` + ServerName string `json:"serverName"` +} + +type PermissionDecisionApprovedForSessionApprovalMemory struct { + Kind PermissionDecisionApprovedForLocationApprovalMemoryKind `json:"kind"` +} + +type PermissionDecisionApprovedForSessionApprovalWrite struct { + Kind PermissionDecisionApprovedForLocationApprovalWriteKind `json:"kind"` +} + type PermissionDecisionDeniedByContentExclusionPolicy struct { // Denied by the organization's content exclusion policy Kind PermissionDecisionDeniedByContentExclusionPolicyKind `json:"kind"` @@ -781,6 +970,24 @@ type PermissionRequestResult struct { Success bool `json:"success"` } +type PermissionsResetSessionApprovalsRequest struct { +} + +type PermissionsResetSessionApprovalsResult struct { + // Whether the operation succeeded + Success bool `json:"success"` +} + +type PermissionsSetApproveAllRequest struct { + // Whether to auto-approve all tool permission requests + Enabled bool `json:"enabled"` +} + +type PermissionsSetApproveAllResult struct { + // Whether the operation succeeded + Success bool `json:"success"` +} + type PingRequest struct { // Optional message to echo back Message *string `json:"message,omitempty"` @@ -854,6 +1061,21 @@ type ServerSkillList struct { Skills []ServerSkill `json:"skills"` } +type SessionAuthStatus struct { + // Authentication type + AuthType *AuthInfoType `json:"authType,omitempty"` + // Copilot plan tier (e.g., individual_pro, business) + CopilotPlan *string `json:"copilotPlan,omitempty"` + // Authentication host URL + Host *string `json:"host,omitempty"` + // Whether the session has resolved authentication + IsAuthenticated bool `json:"isAuthenticated"` + // Authenticated login/username, if available + Login *string `json:"login,omitempty"` + // Human-readable authentication status description + StatusMessage *string `json:"statusMessage,omitempty"` +} + type SessionFSAppendFileRequest struct { // Content to append Content string `json:"content"` @@ -1414,6 +1636,19 @@ type WorkspacesReadFileResult struct { Content string `json:"content"` } +// Authentication type +type AuthInfoType string + +const ( + AuthInfoTypeAPIKey AuthInfoType = "api-key" + AuthInfoTypeUser AuthInfoType = "user" + AuthInfoTypeCopilotAPIToken AuthInfoType = "copilot-api-token" + AuthInfoTypeEnv AuthInfoType = "env" + AuthInfoTypeGhCli AuthInfoType = "gh-cli" + AuthInfoTypeHmac AuthInfoType = "hmac" + AuthInfoTypeToken AuthInfoType = "token" +) + // Configuration source // // Configuration source: user, workspace, plugin, or builtin @@ -1431,9 +1666,9 @@ type DiscoveredMCPServerType string const ( DiscoveredMCPServerTypeHTTP DiscoveredMCPServerType = "http" + DiscoveredMCPServerTypeMemory DiscoveredMCPServerType = "memory" DiscoveredMCPServerTypeSSE DiscoveredMCPServerType = "sse" DiscoveredMCPServerTypeStdio DiscoveredMCPServerType = "stdio" - DiscoveredMCPServerTypeMemory DiscoveredMCPServerType = "memory" ) // Discovery source: project (.github/extensions/) or user (~/.copilot/extensions/) @@ -1539,10 +1774,23 @@ const ( SessionModePlan SessionMode = "plan" ) +type ApprovalKind string + +const ( + ApprovalKindCommands ApprovalKind = "commands" + ApprovalKindCustomTool ApprovalKind = "custom-tool" + ApprovalKindMcp ApprovalKind = "mcp" + ApprovalKindMcpSampling ApprovalKind = "mcp-sampling" + ApprovalKindMemory ApprovalKind = "memory" + ApprovalKindWrite ApprovalKind = "write" +) + type PermissionDecisionKind string const ( PermissionDecisionKindApproved PermissionDecisionKind = "approved" + PermissionDecisionKindApprovedForLocation PermissionDecisionKind = "approved-for-location" + PermissionDecisionKindApprovedForSession PermissionDecisionKind = "approved-for-session" PermissionDecisionKindDeniedByContentExclusionPolicy PermissionDecisionKind = "denied-by-content-exclusion-policy" PermissionDecisionKindDeniedByPermissionRequestHook PermissionDecisionKind = "denied-by-permission-request-hook" PermissionDecisionKindDeniedByRules PermissionDecisionKind = "denied-by-rules" @@ -1556,6 +1804,54 @@ const ( PermissionDecisionApprovedKindApproved PermissionDecisionApprovedKind = "approved" ) +type PermissionDecisionApprovedForLocationKind string + +const ( + PermissionDecisionApprovedForLocationKindApprovedForLocation PermissionDecisionApprovedForLocationKind = "approved-for-location" +) + +type PermissionDecisionApprovedForLocationApprovalCommandsKind string + +const ( + PermissionDecisionApprovedForLocationApprovalCommandsKindCommands PermissionDecisionApprovedForLocationApprovalCommandsKind = "commands" +) + +type PermissionDecisionApprovedForLocationApprovalCustomToolKind string + +const ( + PermissionDecisionApprovedForLocationApprovalCustomToolKindCustomTool PermissionDecisionApprovedForLocationApprovalCustomToolKind = "custom-tool" +) + +type PermissionDecisionApprovedForLocationApprovalMCPKind string + +const ( + PermissionDecisionApprovedForLocationApprovalMCPKindMcp PermissionDecisionApprovedForLocationApprovalMCPKind = "mcp" +) + +type PermissionDecisionApprovedForLocationApprovalMCPSamplingKind string + +const ( + PermissionDecisionApprovedForLocationApprovalMCPSamplingKindMcpSampling PermissionDecisionApprovedForLocationApprovalMCPSamplingKind = "mcp-sampling" +) + +type PermissionDecisionApprovedForLocationApprovalMemoryKind string + +const ( + PermissionDecisionApprovedForLocationApprovalMemoryKindMemory PermissionDecisionApprovedForLocationApprovalMemoryKind = "memory" +) + +type PermissionDecisionApprovedForLocationApprovalWriteKind string + +const ( + PermissionDecisionApprovedForLocationApprovalWriteKindWrite PermissionDecisionApprovedForLocationApprovalWriteKind = "write" +) + +type PermissionDecisionApprovedForSessionKind string + +const ( + PermissionDecisionApprovedForSessionKindApprovedForSession PermissionDecisionApprovedForSessionKind = "approved-for-session" +) + type PermissionDecisionDeniedByContentExclusionPolicyKind string const ( @@ -1717,8 +2013,8 @@ type serverApi struct { type ServerModelsApi serverApi -func (a *ServerModelsApi) List(ctx context.Context) (*ModelList, error) { - raw, err := a.client.Request("models.list", nil) +func (a *ServerModelsApi) List(ctx context.Context, params *ModelsListRequest) (*ModelList, error) { + raw, err := a.client.Request("models.list", params) if err != nil { return nil, err } @@ -1745,8 +2041,8 @@ func (a *ServerToolsApi) List(ctx context.Context, params *ToolsListRequest) (*T type ServerAccountApi serverApi -func (a *ServerAccountApi) GetQuota(ctx context.Context) (*AccountGetQuotaResult, error) { - raw, err := a.client.Request("account.getQuota", nil) +func (a *ServerAccountApi) GetQuota(ctx context.Context, params *AccountGetQuotaRequest) (*AccountGetQuotaResult, error) { + raw, err := a.client.Request("account.getQuota", params) if err != nil { return nil, err } @@ -1821,6 +2117,30 @@ func (a *ServerMcpConfigApi) Remove(ctx context.Context, params *MCPConfigRemove return &result, nil } +func (a *ServerMcpConfigApi) Enable(ctx context.Context, params *MCPConfigEnableRequest) (*MCPConfigEnableResult, error) { + raw, err := a.client.Request("mcp.config.enable", params) + if err != nil { + return nil, err + } + var result MCPConfigEnableResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *ServerMcpConfigApi) Disable(ctx context.Context, params *MCPConfigDisableRequest) (*MCPConfigDisableResult, error) { + raw, err := a.client.Request("mcp.config.disable", params) + if err != nil { + return nil, err + } + var result MCPConfigDisableResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + func (s *ServerMcpApi) Config() *ServerMcpConfigApi { return (*ServerMcpConfigApi)(s) } @@ -1929,6 +2249,21 @@ type sessionApi struct { sessionID string } +type AuthApi sessionApi + +func (a *AuthApi) GetStatus(ctx context.Context) (*SessionAuthStatus, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request("session.auth.getStatus", req) + if err != nil { + return nil, err + } + var result SessionAuthStatus + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + type ModelApi sessionApi func (a *ModelApi) GetCurrent(ctx context.Context) (*CurrentModel, error) { @@ -2362,6 +2697,39 @@ func (a *McpApi) Reload(ctx context.Context) (*MCPReloadResult, error) { return &result, nil } +// Experimental: McpOauthApi contains experimental APIs that may change or be removed. +type McpOauthApi sessionApi + +func (a *McpOauthApi) Login(ctx context.Context, params *MCPOauthLoginRequest) (*MCPOauthLoginResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["serverName"] = params.ServerName + if params.ForceReauth != nil { + req["forceReauth"] = *params.ForceReauth + } + if params.ClientName != nil { + req["clientName"] = *params.ClientName + } + if params.CallbackSuccessMessage != nil { + req["callbackSuccessMessage"] = *params.CallbackSuccessMessage + } + } + raw, err := a.client.Request("session.mcp.oauth.login", req) + if err != nil { + return nil, err + } + var result MCPOauthLoginResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: Oauth returns experimental APIs that may change or be removed. +func (s *McpApi) Oauth() *McpOauthApi { + return (*McpOauthApi)(s) +} + // Experimental: PluginsApi contains experimental APIs that may change or be removed. type PluginsApi sessionApi @@ -2539,6 +2907,35 @@ func (a *PermissionsApi) HandlePendingPermissionRequest(ctx context.Context, par return &result, nil } +func (a *PermissionsApi) SetApproveAll(ctx context.Context, params *PermissionsSetApproveAllRequest) (*PermissionsSetApproveAllResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["enabled"] = params.Enabled + } + raw, err := a.client.Request("session.permissions.setApproveAll", req) + if err != nil { + return nil, err + } + var result PermissionsSetApproveAllResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *PermissionsApi) ResetSessionApprovals(ctx context.Context) (*PermissionsResetSessionApprovalsResult, error) { + req := map[string]any{"sessionId": a.sessionID} + raw, err := a.client.Request("session.permissions.resetSessionApprovals", req) + if err != nil { + return nil, err + } + var result PermissionsResetSessionApprovalsResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + type ShellApi sessionApi func (a *ShellApi) Exec(ctx context.Context, params *ShellExecRequest) (*ShellExecResult, error) { @@ -2634,6 +3031,7 @@ func (a *UsageApi) GetMetrics(ctx context.Context) (*UsageGetMetricsResult, erro type SessionRpc struct { common sessionApi // Reuse a single struct instead of allocating one for each service on the heap. + Auth *AuthApi Model *ModelApi Mode *ModeApi Name *NameApi @@ -2683,6 +3081,7 @@ func (a *SessionRpc) Log(ctx context.Context, params *LogRequest) (*LogResult, e func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc { r := &SessionRpc{} r.common = sessionApi{client: client, sessionID: sessionID} + r.Auth = (*AuthApi)(&r.common) r.Model = (*ModelApi)(&r.common) r.Mode = (*ModeApi)(&r.common) r.Name = (*NameApi)(&r.common) diff --git a/go/types.go b/go/types.go index e11d21402..b47b82faf 100644 --- a/go/types.go +++ b/go/types.go @@ -582,6 +582,10 @@ type SessionConfig struct { // When provided, the server may call back to this client for form-based UI dialogs // (e.g. from MCP tools). Also enables the elicitation capability on the session. OnElicitationRequest ElicitationHandler + // GitHubToken is an optional per-session GitHub token used for authentication. + // When provided, the session authenticates as the token's owner instead of + // using the global client-level auth. + GitHubToken string `json:"-"` } type Tool struct { Name string `json:"name"` @@ -781,6 +785,10 @@ type ResumeSessionConfig struct { DisabledSkills []string // InfiniteSessions configures infinite sessions for persistent workspaces and automatic compaction. InfiniteSessions *InfiniteSessionConfig + // GitHubToken is an optional per-session GitHub token used for authentication. + // When provided, the session authenticates as the token's owner instead of + // using the global client-level auth. + GitHubToken string `json:"-"` // DisableResume, when true, skips emitting the session.resume event. // Useful for reconnecting to a session without triggering resume-related side effects. DisableResume bool @@ -993,6 +1001,7 @@ type createSessionRequest struct { InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` Commands []wireCommand `json:"commands,omitempty"` RequestElicitation *bool `json:"requestElicitation,omitempty"` + GitHubToken string `json:"gitHubToken,omitempty"` Traceparent string `json:"traceparent,omitempty"` Tracestate string `json:"tracestate,omitempty"` } @@ -1041,6 +1050,7 @@ type resumeSessionRequest struct { InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"` Commands []wireCommand `json:"commands,omitempty"` RequestElicitation *bool `json:"requestElicitation,omitempty"` + GitHubToken string `json:"gitHubToken,omitempty"` Traceparent string `json:"traceparent,omitempty"` Tracestate string `json:"tracestate,omitempty"` } diff --git a/nodejs/README.md b/nodejs/README.md index 20e91adbf..af978454c 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -86,8 +86,8 @@ new CopilotClient(options?: CopilotClientOptions) - `useStdio?: boolean` - Use stdio transport instead of TCP (default: true) - `logLevel?: string` - Log level (default: "info") - `autoStart?: boolean` - Auto-start server (default: true) -- `githubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods. -- `useLoggedInUser?: boolean` - Whether to use logged-in user for authentication (default: true, but false when `githubToken` is provided). Cannot be used with `cliUrl`. +- `gitHubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods. +- `useLoggedInUser?: boolean` - Whether to use logged-in user for authentication (default: true, but false when `gitHubToken` is provided). Cannot be used with `cliUrl`. - `telemetry?: TelemetryConfig` - OpenTelemetry configuration for the CLI process. Providing this object enables telemetry — no separate flag needed. See [Telemetry](#telemetry) below. - `onGetTraceContext?: TraceContextProvider` - Advanced: callback for linking your application's own OpenTelemetry spans into the same distributed trace as the CLI's spans. Not needed for normal telemetry collection. See [Telemetry](#telemetry) below. diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 574bc86a9..f34ced6e6 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.35-0", + "@github/copilot": "^1.0.35-5", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -663,26 +663,26 @@ } }, "node_modules/@github/copilot": { - "version": "1.0.35-0", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.35-0.tgz", - "integrity": "sha512-daPkiDXeXwsoEHy4XvZywVX3Voyaubir27qm/3uyifxeruMGOcUT/XC8tkJhE6VfSy3nvtjV4xXrZ43Wr0x2cg==", + "version": "1.0.35-5", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-1.0.35-5.tgz", + "integrity": "sha512-/teEQVQOtC+oAsAKHoJBgPSO8HEk93pBxY9jCcJaATJ6ayl1oGeOAqdrmDUK1UUvjOQsLM6Eui8C2D8B7AE8Aw==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "1.0.35-0", - "@github/copilot-darwin-x64": "1.0.35-0", - "@github/copilot-linux-arm64": "1.0.35-0", - "@github/copilot-linux-x64": "1.0.35-0", - "@github/copilot-win32-arm64": "1.0.35-0", - "@github/copilot-win32-x64": "1.0.35-0" + "@github/copilot-darwin-arm64": "1.0.35-5", + "@github/copilot-darwin-x64": "1.0.35-5", + "@github/copilot-linux-arm64": "1.0.35-5", + "@github/copilot-linux-x64": "1.0.35-5", + "@github/copilot-win32-arm64": "1.0.35-5", + "@github/copilot-win32-x64": "1.0.35-5" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "1.0.35-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.35-0.tgz", - "integrity": "sha512-Uc3PIw60y/9fk1F2JlLqBl0VkParTiCIxlLWKFs8N6TJwFafKmLt7B5r4nqoFhsYZOov6ww4nIxxaMiVdFF0YA==", + "version": "1.0.35-5", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-1.0.35-5.tgz", + "integrity": "sha512-98X8ygCOamqMFQ1t4610BJNt/YH5AdQwvUuQbAj6L4svaZgP7UM2/p8ZxZThvoEu/fyxrB6YCz8upwAplClVWQ==", "cpu": [ "arm64" ], @@ -696,9 +696,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "1.0.35-0", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.35-0.tgz", - "integrity": "sha512-5R5hkZ4Z2CnHVdXnKMNjkFi00mdBYF9H9kkzQjmaN8cG4JwZFf209lo1bEzpXWKHl136LXNwLVhHCYfi3FgzXQ==", + "version": "1.0.35-5", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-1.0.35-5.tgz", + "integrity": "sha512-VTKEHjYT4UFt6FBIgBZjI1hZhoVNOayLIkiBi41QltxC6dA2XO0BDanxZZewkWLLY+NZUkY9D9OpLIqMvgBQkA==", "cpu": [ "x64" ], @@ -712,9 +712,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "1.0.35-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.35-0.tgz", - "integrity": "sha512-I+kDV2xhvq2t6ux2/ZmWoRkReq8fNlYgW1GfWRmp4c+vQKvH+WsQ5P0WWSt8BmmQGK9hUrTcXg2nvVAPQJ2D8Q==", + "version": "1.0.35-5", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-1.0.35-5.tgz", + "integrity": "sha512-PLYP2LWbL/nxPANQPsiYiX5bRxULlwsPxQ/hZq6U55CBaK9ayMiwUULuZvaklnaEeFlU5z5Ob43DoN9H7bOdbQ==", "cpu": [ "arm64" ], @@ -728,9 +728,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "1.0.35-0", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.35-0.tgz", - "integrity": "sha512-mnG6lpzmWvkasdYgmvotb2PQKW/GaCAdZbuv34iOT84Iz3VyEamcUNurw+KCrxitCYRa68cnCQFbGMf8p6Q22A==", + "version": "1.0.35-5", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-1.0.35-5.tgz", + "integrity": "sha512-NI+UaacwA3iqb1ArETHsrSYsFQWCHNo6exwV7Li4vJvfw3xCNRNeV7Wuna/LM0VFXZVZfXjnDdSzKeXiqrlWKA==", "cpu": [ "x64" ], @@ -744,9 +744,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "1.0.35-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.35-0.tgz", - "integrity": "sha512-suB5kxHQtD5Hu7NUqH3bUkNBg6e0rPLSf54jCN8UjyxJBfV2mL7BZeqr77Du3UzHHkRKxqITiZ4LBZH8q0bOEg==", + "version": "1.0.35-5", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-1.0.35-5.tgz", + "integrity": "sha512-T0jj4SjWWeD/hQwDY4cVx0UE0Tn3DkFUoPefErLyvh2qclZF5nuFCSehftwGMeQbxZP8PhmEjiCZu/l2wBlD4A==", "cpu": [ "arm64" ], @@ -760,9 +760,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "1.0.35-0", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.35-0.tgz", - "integrity": "sha512-KKuxw+rKpfEn/575l+3aef72/MiGlH8D9CIX6+3+qPQqojt7YBDlEqgL3/aAk9JUrQbiqSUXXKD3mMEHdgNoWQ==", + "version": "1.0.35-5", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-1.0.35-5.tgz", + "integrity": "sha512-o0Rh2z1xI2zq7VWjR7wgXIEjTuTzUfDHYMf1x5lGXFpRy8SQKvqimuS+nERks1Wamambp7S70OmjkBLk/Iw/IA==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index c33b8cb2c..53eba298e 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -56,7 +56,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^1.0.35-0", + "@github/copilot": "^1.0.35-5", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index a8eba8c37..527758f98 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -215,7 +215,7 @@ export class CopilotClient { CopilotClientOptions, | "cliPath" | "cliUrl" - | "githubToken" + | "gitHubToken" | "useLoggedInUser" | "onListModels" | "telemetry" @@ -225,7 +225,7 @@ export class CopilotClient { > & { cliPath?: string; cliUrl?: string; - githubToken?: string; + gitHubToken?: string; useLoggedInUser?: boolean; telemetry?: TelemetryConfig; }; @@ -294,9 +294,9 @@ export class CopilotClient { } // Validate auth options with external server - if (options.cliUrl && (options.githubToken || options.useLoggedInUser !== undefined)) { + 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)" + "gitHubToken and useLoggedInUser cannot be used with cliUrl (external server manages its own auth)" ); } @@ -336,9 +336,9 @@ export class CopilotClient { autoRestart: false, env: effectiveEnv, - githubToken: options.githubToken, - // Default useLoggedInUser to false when githubToken is provided, otherwise true - useLoggedInUser: options.useLoggedInUser ?? (options.githubToken ? false : true), + gitHubToken: options.gitHubToken, + // Default useLoggedInUser to false when gitHubToken is provided, otherwise true + useLoggedInUser: options.useLoggedInUser ?? (options.gitHubToken ? false : true), telemetry: options.telemetry, }; } @@ -762,6 +762,7 @@ export class CopilotClient { skillDirectories: config.skillDirectories, disabledSkills: config.disabledSkills, infiniteSessions: config.infiniteSessions, + gitHubToken: config.gitHubToken, }); const { workspacePath, capabilities } = response as { @@ -905,6 +906,7 @@ export class CopilotClient { disabledSkills: config.disabledSkills, infiniteSessions: config.infiniteSessions, disableResume: config.disableResume, + gitHubToken: config.gitHubToken, }); const { workspacePath, capabilities } = response as { @@ -1407,7 +1409,7 @@ export class CopilotClient { } // Add auth-related flags - if (this.options.githubToken) { + if (this.options.gitHubToken) { args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN"); } if (!this.options.useLoggedInUser) { @@ -1419,8 +1421,8 @@ export class CopilotClient { delete envWithoutNodeDebug.NODE_DEBUG; // Set auth token in environment if provided - if (this.options.githubToken) { - envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.githubToken; + if (this.options.gitHubToken) { + envWithoutNodeDebug.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken; } if (!this.options.cliPath) { diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index b40ffc701..543f82934 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -5,6 +5,13 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; +/** + * Authentication type + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "AuthInfoType". + */ +export type AuthInfoType = "hmac" | "env" | "user" | "gh-cli" | "api-key" | "token" | "copilot-api-token"; /** * Server transport type: stdio, http, sse, or memory (local configs are normalized to stdio) * @@ -104,11 +111,39 @@ export type SessionMode = "interactive" | "plan" | "autopilot"; export type PermissionDecision = | PermissionDecisionApproved + | PermissionDecisionApprovedForSession + | PermissionDecisionApprovedForLocation | PermissionDecisionDeniedByRules | PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser | PermissionDecisionDeniedInteractivelyByUser | PermissionDecisionDeniedByContentExclusionPolicy | PermissionDecisionDeniedByPermissionRequestHook; +/** + * The approval to add as a session-scoped rule + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "PermissionDecisionApprovedForSessionApproval". + */ +export type PermissionDecisionApprovedForSessionApproval = + | PermissionDecisionApprovedForSessionApprovalCommands + | PermissionDecisionApprovedForSessionApprovalWrite + | PermissionDecisionApprovedForSessionApprovalMcp + | PermissionDecisionApprovedForSessionApprovalMcpSampling + | PermissionDecisionApprovedForSessionApprovalMemory + | PermissionDecisionApprovedForSessionApprovalCustomTool; +/** + * The approval to persist for this location + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "PermissionDecisionApprovedForLocationApproval". + */ +export type PermissionDecisionApprovedForLocationApproval = + | PermissionDecisionApprovedForLocationApprovalCommands + | PermissionDecisionApprovedForLocationApprovalWrite + | PermissionDecisionApprovedForLocationApprovalMcp + | PermissionDecisionApprovedForLocationApprovalMcpSampling + | PermissionDecisionApprovedForLocationApprovalMemory + | PermissionDecisionApprovedForLocationApprovalCustomTool; /** * Error classification * @@ -167,6 +202,13 @@ export type UIElicitationSchemaPropertyNumberType = "number" | "integer"; */ export type UIElicitationResponseAction = "accept" | "decline" | "cancel"; +export interface AccountGetQuotaRequest { + /** + * GitHub token for per-user quota lookup. When provided, resolves this token to determine the user's quota instead of using the global auth. + */ + gitHubToken?: string; +} + export interface AccountGetQuotaResult { /** * Quota snapshots keyed by type (e.g., chat, completions, premium_interactions) @@ -537,6 +579,20 @@ export interface McpServerConfigHttp { oauthPublicClient?: boolean; } +export interface McpConfigDisableRequest { + /** + * Names of MCP servers to disable. Each server is added to the persisted disabled list so new sessions skip it. Already-disabled names are ignored. Active sessions keep their current connections until they end. + */ + names: string[]; +} + +export interface McpConfigEnableRequest { + /** + * Names of MCP servers to enable. Each server is removed from the persisted disabled list so new sessions spawn it. Unknown or already-enabled names are ignored. + */ + names: string[]; +} + export interface McpConfigList { /** * All MCP servers from user config, keyed by name @@ -591,6 +647,34 @@ export interface McpEnableRequest { serverName: string; } +/** @experimental */ +export interface McpOauthLoginRequest { + /** + * Name of the remote MCP server to authenticate + */ + serverName: string; + /** + * When true, clears any cached OAuth token for the server and runs a full new authorization. Use when the user explicitly wants to switch accounts or believes their session is stuck. + */ + forceReauth?: boolean; + /** + * Optional override for the OAuth client display name shown on the consent screen. Applies to newly registered dynamic clients only — existing registrations keep the name they were created with. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass their own surface-specific label so the consent screen matches the product the user sees. + */ + clientName?: string; + /** + * Optional override for the body text shown on the OAuth loopback callback success page. When omitted, the runtime applies a neutral fallback; callers driving interactive auth should pass surface-specific copy telling the user where to return. + */ + callbackSuccessMessage?: string; +} + +/** @experimental */ +export interface McpOauthLoginResult { + /** + * URL the caller should open in a browser to complete OAuth. Omitted when cached tokens were still valid and no browser interaction was needed — the server is already reconnected in that case. When present, the runtime starts the callback listener before returning and continues the flow in the background; completion is signaled via session.mcp_server_status_changed. + */ + authorizationUrl?: string; +} + export interface McpServer { /** * Server name (config key) @@ -714,7 +798,7 @@ export interface ModelPolicy { /** * Usage terms or conditions for this model */ - terms: string; + terms?: string; } /** * Billing information @@ -786,6 +870,13 @@ export interface ModelList { models: Model[]; } +export interface ModelsListRequest { + /** + * GitHub token for per-user model listing. When provided, resolves this token to determine the user's Copilot plan and available models instead of using the global auth. + */ + gitHubToken?: string; +} + export interface ModelSwitchToRequest { /** * Model identifier to switch to @@ -830,6 +921,84 @@ export interface PermissionDecisionApproved { kind: "approved"; } +export interface PermissionDecisionApprovedForSession { + /** + * Approved and remembered for the rest of the session + */ + kind: "approved-for-session"; + approval: PermissionDecisionApprovedForSessionApproval; +} + +export interface PermissionDecisionApprovedForSessionApprovalCommands { + kind: "commands"; + commandIdentifiers: string[]; +} + +export interface PermissionDecisionApprovedForSessionApprovalWrite { + kind: "write"; +} + +export interface PermissionDecisionApprovedForSessionApprovalMcp { + kind: "mcp"; + serverName: string; + toolName: string | null; +} + +export interface PermissionDecisionApprovedForSessionApprovalMcpSampling { + kind: "mcp-sampling"; + serverName: string; +} + +export interface PermissionDecisionApprovedForSessionApprovalMemory { + kind: "memory"; +} + +export interface PermissionDecisionApprovedForSessionApprovalCustomTool { + kind: "custom-tool"; + toolName: string; +} + +export interface PermissionDecisionApprovedForLocation { + /** + * Approved and persisted for this project location + */ + kind: "approved-for-location"; + approval: PermissionDecisionApprovedForLocationApproval; + /** + * The location key (git root or cwd) to persist the approval to + */ + locationKey: string; +} + +export interface PermissionDecisionApprovedForLocationApprovalCommands { + kind: "commands"; + commandIdentifiers: string[]; +} + +export interface PermissionDecisionApprovedForLocationApprovalWrite { + kind: "write"; +} + +export interface PermissionDecisionApprovedForLocationApprovalMcp { + kind: "mcp"; + serverName: string; + toolName: string | null; +} + +export interface PermissionDecisionApprovedForLocationApprovalMcpSampling { + kind: "mcp-sampling"; + serverName: string; +} + +export interface PermissionDecisionApprovedForLocationApprovalMemory { + kind: "memory"; +} + +export interface PermissionDecisionApprovedForLocationApprovalCustomTool { + kind: "custom-tool"; + toolName: string; +} + export interface PermissionDecisionDeniedByRules { /** * Denied because approval rules explicitly blocked it @@ -904,6 +1073,29 @@ export interface PermissionRequestResult { success: boolean; } +export interface PermissionsResetSessionApprovalsRequest {} + +export interface PermissionsResetSessionApprovalsResult { + /** + * Whether the operation succeeded + */ + success: boolean; +} + +export interface PermissionsSetApproveAllRequest { + /** + * Whether to auto-approve all tool permission requests + */ + enabled: boolean; +} + +export interface PermissionsSetApproveAllResult { + /** + * Whether the operation succeeded + */ + success: boolean; +} + export interface PingRequest { /** * Optional message to echo back @@ -1013,6 +1205,30 @@ export interface ServerSkillList { skills: ServerSkill[]; } +export interface SessionAuthStatus { + /** + * Whether the session has resolved authentication + */ + isAuthenticated: boolean; + authType?: AuthInfoType; + /** + * Authentication host URL + */ + host?: string; + /** + * Authenticated login/username, if available + */ + login?: string; + /** + * Human-readable authentication status description + */ + statusMessage?: string; + /** + * Copilot plan tier (e.g., individual_pro, business) + */ + copilotPlan?: string; +} + export interface SessionFsAppendFileRequest { /** * Target session identifier @@ -1769,16 +1985,16 @@ export function createServerRpc(connection: MessageConnection) { ping: async (params: PingRequest): Promise => connection.sendRequest("ping", params), models: { - list: async (): Promise => - connection.sendRequest("models.list", {}), + list: async (params?: ModelsListRequest): Promise => + connection.sendRequest("models.list", params), }, tools: { list: async (params: ToolsListRequest): Promise => connection.sendRequest("tools.list", params), }, account: { - getQuota: async (): Promise => - connection.sendRequest("account.getQuota", {}), + getQuota: async (params?: AccountGetQuotaRequest): Promise => + connection.sendRequest("account.getQuota", params), }, mcp: { config: { @@ -1790,6 +2006,10 @@ export function createServerRpc(connection: MessageConnection) { connection.sendRequest("mcp.config.update", params), remove: async (params: McpConfigRemoveRequest): Promise => connection.sendRequest("mcp.config.remove", params), + enable: async (params: McpConfigEnableRequest): Promise => + connection.sendRequest("mcp.config.enable", params), + disable: async (params: McpConfigDisableRequest): Promise => + connection.sendRequest("mcp.config.disable", params), }, discover: async (params: McpDiscoverRequest): Promise => connection.sendRequest("mcp.discover", params), @@ -1817,28 +2037,32 @@ export function createServerRpc(connection: MessageConnection) { /** Create typed session-scoped RPC methods. */ export function createSessionRpc(connection: MessageConnection, sessionId: string) { return { + auth: { + getStatus: async (): Promise => + connection.sendRequest("session.auth.getStatus", { sessionId }), + }, model: { getCurrent: async (): Promise => connection.sendRequest("session.model.getCurrent", { sessionId }), - switchTo: async (params: Omit): Promise => + switchTo: async (params: ModelSwitchToRequest): Promise => connection.sendRequest("session.model.switchTo", { sessionId, ...params }), }, mode: { get: async (): Promise => connection.sendRequest("session.mode.get", { sessionId }), - set: async (params: Omit): Promise => + set: async (params: ModeSetRequest): Promise => connection.sendRequest("session.mode.set", { sessionId, ...params }), }, name: { get: async (): Promise => connection.sendRequest("session.name.get", { sessionId }), - set: async (params: Omit): Promise => + set: async (params: NameSetRequest): Promise => connection.sendRequest("session.name.set", { sessionId, ...params }), }, plan: { read: async (): Promise => connection.sendRequest("session.plan.read", { sessionId }), - update: async (params: Omit): Promise => + update: async (params: PlanUpdateRequest): Promise => connection.sendRequest("session.plan.update", { sessionId, ...params }), delete: async (): Promise => connection.sendRequest("session.plan.delete", { sessionId }), @@ -1848,9 +2072,9 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.workspaces.getWorkspace", { sessionId }), listFiles: async (): Promise => connection.sendRequest("session.workspaces.listFiles", { sessionId }), - readFile: async (params: Omit): Promise => + readFile: async (params: WorkspacesReadFileRequest): Promise => connection.sendRequest("session.workspaces.readFile", { sessionId, ...params }), - createFile: async (params: Omit): Promise => + createFile: async (params: WorkspacesCreateFileRequest): Promise => connection.sendRequest("session.workspaces.createFile", { sessionId, ...params }), }, instructions: { @@ -1859,7 +2083,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin }, /** @experimental */ fleet: { - start: async (params: Omit): Promise => + start: async (params: FleetStartRequest): Promise => connection.sendRequest("session.fleet.start", { sessionId, ...params }), }, /** @experimental */ @@ -1868,7 +2092,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.agent.list", { sessionId }), getCurrent: async (): Promise => connection.sendRequest("session.agent.getCurrent", { sessionId }), - select: async (params: Omit): Promise => + select: async (params: AgentSelectRequest): Promise => connection.sendRequest("session.agent.select", { sessionId, ...params }), deselect: async (): Promise => connection.sendRequest("session.agent.deselect", { sessionId }), @@ -1879,9 +2103,9 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin skills: { list: async (): Promise => connection.sendRequest("session.skills.list", { sessionId }), - enable: async (params: Omit): Promise => + enable: async (params: SkillsEnableRequest): Promise => connection.sendRequest("session.skills.enable", { sessionId, ...params }), - disable: async (params: Omit): Promise => + disable: async (params: SkillsDisableRequest): Promise => connection.sendRequest("session.skills.disable", { sessionId, ...params }), reload: async (): Promise => connection.sendRequest("session.skills.reload", { sessionId }), @@ -1890,12 +2114,17 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin mcp: { list: async (): Promise => connection.sendRequest("session.mcp.list", { sessionId }), - enable: async (params: Omit): Promise => + enable: async (params: McpEnableRequest): Promise => connection.sendRequest("session.mcp.enable", { sessionId, ...params }), - disable: async (params: Omit): Promise => + disable: async (params: McpDisableRequest): Promise => connection.sendRequest("session.mcp.disable", { sessionId, ...params }), reload: async (): Promise => connection.sendRequest("session.mcp.reload", { sessionId }), + /** @experimental */ + oauth: { + login: async (params: McpOauthLoginRequest): Promise => + connection.sendRequest("session.mcp.oauth.login", { sessionId, ...params }), + }, }, /** @experimental */ plugins: { @@ -1906,44 +2135,48 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin extensions: { list: async (): Promise => connection.sendRequest("session.extensions.list", { sessionId }), - enable: async (params: Omit): Promise => + enable: async (params: ExtensionsEnableRequest): Promise => connection.sendRequest("session.extensions.enable", { sessionId, ...params }), - disable: async (params: Omit): Promise => + disable: async (params: ExtensionsDisableRequest): Promise => connection.sendRequest("session.extensions.disable", { sessionId, ...params }), reload: async (): Promise => connection.sendRequest("session.extensions.reload", { sessionId }), }, tools: { - handlePendingToolCall: async (params: Omit): Promise => + handlePendingToolCall: async (params: ToolsHandlePendingToolCallRequest): Promise => connection.sendRequest("session.tools.handlePendingToolCall", { sessionId, ...params }), }, commands: { - handlePendingCommand: async (params: Omit): Promise => + handlePendingCommand: async (params: CommandsHandlePendingCommandRequest): Promise => connection.sendRequest("session.commands.handlePendingCommand", { sessionId, ...params }), }, ui: { - elicitation: async (params: Omit): Promise => + elicitation: async (params: UIElicitationRequest): Promise => connection.sendRequest("session.ui.elicitation", { sessionId, ...params }), - handlePendingElicitation: async (params: Omit): Promise => + handlePendingElicitation: async (params: UIHandlePendingElicitationRequest): Promise => connection.sendRequest("session.ui.handlePendingElicitation", { sessionId, ...params }), }, permissions: { - handlePendingPermissionRequest: async (params: Omit): Promise => + handlePendingPermissionRequest: async (params: PermissionDecisionRequest): Promise => connection.sendRequest("session.permissions.handlePendingPermissionRequest", { sessionId, ...params }), + setApproveAll: async (params: PermissionsSetApproveAllRequest): Promise => + connection.sendRequest("session.permissions.setApproveAll", { sessionId, ...params }), + resetSessionApprovals: async (): Promise => + connection.sendRequest("session.permissions.resetSessionApprovals", { sessionId }), }, - log: async (params: Omit): Promise => + log: async (params: LogRequest): Promise => connection.sendRequest("session.log", { sessionId, ...params }), shell: { - exec: async (params: Omit): Promise => + exec: async (params: ShellExecRequest): Promise => connection.sendRequest("session.shell.exec", { sessionId, ...params }), - kill: async (params: Omit): Promise => + kill: async (params: ShellKillRequest): Promise => connection.sendRequest("session.shell.kill", { sessionId, ...params }), }, /** @experimental */ history: { compact: async (): Promise => connection.sendRequest("session.history.compact", { sessionId }), - truncate: async (params: Omit): Promise => + truncate: async (params: HistoryTruncateRequest): Promise => connection.sendRequest("session.history.truncate", { sessionId, ...params }), }, /** @experimental */ diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index b35ab7c59..c3a54b85f 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -67,6 +67,8 @@ export type SessionEvent = | CommandQueuedEvent | CommandExecuteEvent | CommandCompletedEvent + | AutoModeSwitchRequestedEvent + | AutoModeSwitchCompletedEvent | CommandsChangedEvent | CapabilitiesChangedEvent | ExitPlanModeRequestedEvent @@ -179,6 +181,8 @@ export type PermissionRequestMemoryDirection = "upvote" | "downvote"; */ export type PermissionCompletedKind = | "approved" + | "approved-for-session" + | "approved-for-location" | "denied-by-rules" | "denied-no-approval-rule-and-could-not-request-from-user" | "denied-interactively-by-user" @@ -1251,21 +1255,68 @@ export interface CompactionCompleteData { toolDefinitionsTokens?: number; } /** - * Token usage breakdown for the compaction LLM call + * Token usage breakdown for the compaction LLM call (aligned with assistant.usage format) */ export interface CompactionCompleteCompactionTokensUsed { /** * Cached input tokens reused in the compaction LLM call */ - cachedInput: number; + cacheReadTokens?: number; + /** + * Tokens written to prompt cache in the compaction LLM call + */ + cacheWriteTokens?: number; + copilotUsage?: CompactionCompleteCompactionTokensUsedCopilotUsage; + /** + * Duration of the compaction LLM call in milliseconds + */ + duration?: number; /** * Input tokens consumed by the compaction LLM call */ - input: number; + inputTokens?: number; + /** + * Model identifier used for the compaction LLM call + */ + model?: string; /** * Output tokens produced by the compaction LLM call */ - output: number; + outputTokens?: number; +} +/** + * Per-request cost and usage data from the CAPI copilot_usage response field + */ +export interface CompactionCompleteCompactionTokensUsedCopilotUsage { + /** + * Itemized token usage breakdown + */ + tokenDetails: CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail[]; + /** + * Total cost in nano-AIU (AI Units) for this request + */ + totalNanoAiu: number; +} +/** + * Token usage detail for a single billing category + */ +export interface CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail { + /** + * Number of tokens in this billing batch + */ + batchSize: number; + /** + * Cost per batch of tokens + */ + costPerBatch: number; + /** + * Total token count for this entry + */ + tokenCount: number; + /** + * Token category (e.g., "input", "output") + */ + tokenType: string; } export interface TaskCompleteEvent { /** @@ -3916,6 +3967,74 @@ export interface CommandCompletedData { */ requestId: string; } +export interface AutoModeSwitchRequestedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: AutoModeSwitchRequestedData; + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + type: "auto_mode_switch.requested"; +} +/** + * Auto mode switch request notification requiring user approval + */ +export interface AutoModeSwitchRequestedData { + /** + * The rate limit error code that triggered this request + */ + errorCode?: string; + /** + * Unique identifier for this request; used to respond via session.respondToAutoModeSwitch() + */ + requestId: string; +} +export interface AutoModeSwitchCompletedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: AutoModeSwitchCompletedData; + ephemeral: true; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + type: "auto_mode_switch.completed"; +} +/** + * Auto mode switch completion notification + */ +export interface AutoModeSwitchCompletedData { + /** + * Request ID of the resolved request; clients should dismiss any UI for this request + */ + requestId: string; + /** + * The user's choice: 'yes', 'yes_always', or 'no' + */ + response: string; +} export interface CommandsChangedEvent { /** * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index 9f6eaf11d..9f2b6d48e 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -126,13 +126,13 @@ export interface CopilotClientOptions { * 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) */ useLoggedInUser?: boolean; @@ -1340,6 +1340,18 @@ export interface SessionConfig { */ infiniteSessions?: InfiniteSessionConfig; + /** + * GitHub token for per-session authentication. + * When provided, the runtime resolves this token into a full GitHub identity + * (login, Copilot plan, endpoints) and stores it on the session. This enables + * multitenancy — different sessions can have different GitHub identities. + * + * This is independent of the client-level `gitHubToken` in {@link CopilotClientOptions}, + * which authenticates the CLI process itself. The session-level token determines + * the identity used for content exclusion, model routing, and quota checks. + */ + gitHubToken?: string; + /** * Optional event handler that is registered on the session before the * session.create RPC is issued. This guarantees that early events emitted @@ -1389,6 +1401,7 @@ export type ResumeSessionConfig = Pick< | "skillDirectories" | "disabledSkills" | "infiniteSessions" + | "gitHubToken" | "onEvent" | "createSessionFsHandler" > & { diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 4ea74b576..eceb26f22 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -530,16 +530,16 @@ describe("CopilotClient", () => { }); describe("Auth options", () => { - it("should accept githubToken option", () => { + it("should accept gitHubToken option", () => { const client = new CopilotClient({ - githubToken: "gho_test_token", + gitHubToken: "gho_test_token", logLevel: "error", }); - expect((client as any).options.githubToken).toBe("gho_test_token"); + expect((client as any).options.gitHubToken).toBe("gho_test_token"); }); - it("should default useLoggedInUser to true when no githubToken", () => { + it("should default useLoggedInUser to true when no gitHubToken", () => { const client = new CopilotClient({ logLevel: "error", }); @@ -547,18 +547,18 @@ describe("CopilotClient", () => { expect((client as any).options.useLoggedInUser).toBe(true); }); - it("should default useLoggedInUser to false when githubToken is provided", () => { + it("should default useLoggedInUser to false when gitHubToken is provided", () => { const client = new CopilotClient({ - githubToken: "gho_test_token", + gitHubToken: "gho_test_token", logLevel: "error", }); expect((client as any).options.useLoggedInUser).toBe(false); }); - it("should allow explicit useLoggedInUser: true with githubToken", () => { + it("should allow explicit useLoggedInUser: true with gitHubToken", () => { const client = new CopilotClient({ - githubToken: "gho_test_token", + gitHubToken: "gho_test_token", useLoggedInUser: true, logLevel: "error", }); @@ -566,7 +566,7 @@ describe("CopilotClient", () => { expect((client as any).options.useLoggedInUser).toBe(true); }); - it("should allow explicit useLoggedInUser: false without githubToken", () => { + it("should allow explicit useLoggedInUser: false without gitHubToken", () => { const client = new CopilotClient({ useLoggedInUser: false, logLevel: "error", @@ -575,14 +575,14 @@ describe("CopilotClient", () => { expect((client as any).options.useLoggedInUser).toBe(false); }); - it("should throw error when githubToken is used with cliUrl", () => { + it("should throw error when gitHubToken is used with cliUrl", () => { expect(() => { new CopilotClient({ cliUrl: "localhost:8080", - githubToken: "gho_test_token", + gitHubToken: "gho_test_token", logLevel: "error", }); - }).toThrow(/githubToken and useLoggedInUser cannot be used with cliUrl/); + }).toThrow(/gitHubToken and useLoggedInUser cannot be used with cliUrl/); }); it("should throw error when useLoggedInUser is used with cliUrl", () => { @@ -592,7 +592,7 @@ describe("CopilotClient", () => { useLoggedInUser: false, logLevel: "error", }); - }).toThrow(/githubToken and useLoggedInUser cannot be used with cliUrl/); + }).toThrow(/gitHubToken and useLoggedInUser cannot be used with cliUrl/); }); }); diff --git a/nodejs/test/e2e/harness/CapiProxy.ts b/nodejs/test/e2e/harness/CapiProxy.ts index f08ffc575..e0a270da1 100644 --- a/nodejs/test/e2e/harness/CapiProxy.ts +++ b/nodejs/test/e2e/harness/CapiProxy.ts @@ -1,7 +1,10 @@ import { spawn } from "child_process"; import { resolve } from "path"; import { expect } from "vitest"; -import { ParsedHttpExchange } from "../../../../test/harness/replayingCapiProxy"; +import { + CopilotUserResponse, + ParsedHttpExchange, +} from "../../../../test/harness/replayingCapiProxy"; const HARNESS_SERVER_PATH = resolve(__dirname, "../../../../test/harness/server.ts"); @@ -50,4 +53,18 @@ export class CapiProxy { const response = await fetch(url, { method: "POST" }); expect(response.ok).toBe(true); } + + /** + * Register a per-token response for the `/copilot_internal/user` endpoint. + * When a request with `Authorization: Bearer ` arrives at the proxy, + * the matching response is returned. + */ + async setCopilotUserByToken(token: string, response: CopilotUserResponse): Promise { + const res = await fetch(`${this.proxyUrl}/copilot-user-config`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ token, response }), + }); + expect(res.ok).toBe(true); + } } diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index c6d413936..d9680a9ba 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -51,7 +51,7 @@ export async function createSdkTestContext({ logLevel: logLevel || "error", cliPath: process.env.COPILOT_CLI_PATH, // Use fake token in CI to allow cached responses without real auth - githubToken: isCI ? "fake-token-for-e2e-tests" : undefined, + gitHubToken: isCI ? "fake-token-for-e2e-tests" : undefined, useStdio: useStdio, ...copilotClientOptions, }); diff --git a/nodejs/test/e2e/per_session_auth.test.ts b/nodejs/test/e2e/per_session_auth.test.ts new file mode 100644 index 000000000..d795f89b2 --- /dev/null +++ b/nodejs/test/e2e/per_session_auth.test.ts @@ -0,0 +1,100 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("Per-session GitHub auth", async () => { + const { copilotClient: client, openAiEndpoint, env } = await createSdkTestContext(); + + // Redirect GitHub API calls (e.g., fetchCopilotUser) to the proxy + // so per-session auth token resolution can be tested + env.COPILOT_DEBUG_GITHUB_API_URL = env.COPILOT_API_URL; + + // Configure per-token responses on the proxy. + // endpoints.api points back to the proxy so subsequent CAPI calls are also intercepted. + const proxyUrl = env.COPILOT_API_URL; + await openAiEndpoint.setCopilotUserByToken("token-alice", { + login: "alice", + copilot_plan: "individual_pro", + endpoints: { + api: proxyUrl, + telemetry: "https://localhost:1/telemetry", + }, + analytics_tracking_id: "alice-tracking-id", + }); + + await openAiEndpoint.setCopilotUserByToken("token-bob", { + login: "bob", + copilot_plan: "business", + endpoints: { + api: proxyUrl, + telemetry: "https://localhost:1/telemetry", + }, + analytics_tracking_id: "bob-tracking-id", + }); + + it("should create session with gitHubToken and check auth status", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + gitHubToken: "token-alice", + }); + + const authStatus = await session.rpc.auth.getStatus(); + expect(authStatus.isAuthenticated).toBe(true); + expect(authStatus.login).toBe("alice"); + expect(authStatus.copilotPlan).toBe("individual_pro"); + + await session.disconnect(); + }); + + it("should isolate auth between sessions with different tokens", async () => { + const sessionA = await client.createSession({ + onPermissionRequest: approveAll, + gitHubToken: "token-alice", + }); + const sessionB = await client.createSession({ + onPermissionRequest: approveAll, + gitHubToken: "token-bob", + }); + + const statusA = await sessionA.rpc.auth.getStatus(); + const statusB = await sessionB.rpc.auth.getStatus(); + + expect(statusA.isAuthenticated).toBe(true); + expect(statusA.login).toBe("alice"); + expect(statusA.copilotPlan).toBe("individual_pro"); + + expect(statusB.isAuthenticated).toBe(true); + expect(statusB.login).toBe("bob"); + expect(statusB.copilotPlan).toBe("business"); + + await sessionA.disconnect(); + await sessionB.disconnect(); + }); + + it("should return unauthenticated when no token is provided", async () => { + const session = await client.createSession({ + onPermissionRequest: approveAll, + }); + + const authStatus = await session.rpc.auth.getStatus(); + // Without a per-session GitHub token, there is no per-session identity. + // In CI the process-level fake token may still authenticate globally, + // so we check login rather than isAuthenticated. + expect(authStatus.login).toBeFalsy(); + + await session.disconnect(); + }); + + it("should error when creating session with invalid token", async () => { + await expect( + client.createSession({ + onPermissionRequest: approveAll, + gitHubToken: "invalid-token-12345", + }) + ).rejects.toThrow(); + }); +}); diff --git a/nodejs/test/e2e/session.test.ts b/nodejs/test/e2e/session.test.ts index 6153d4e4c..f5a181380 100644 --- a/nodejs/test/e2e/session.test.ts +++ b/nodejs/test/e2e/session.test.ts @@ -244,7 +244,7 @@ describe("Sessions", async () => { // Resume using a new client const newClient = new CopilotClient({ env, - githubToken: isCI ? "fake-token-for-e2e-tests" : undefined, + gitHubToken: isCI ? "fake-token-for-e2e-tests" : undefined, }); onTestFinished(() => newClient.forceStop()); diff --git a/nodejs/test/e2e/streaming_fidelity.test.ts b/nodejs/test/e2e/streaming_fidelity.test.ts index 11edee1ca..d91e6c5d8 100644 --- a/nodejs/test/e2e/streaming_fidelity.test.ts +++ b/nodejs/test/e2e/streaming_fidelity.test.ts @@ -83,7 +83,7 @@ describe("Streaming Fidelity", async () => { // Resume using a new client const newClient = new CopilotClient({ env, - githubToken: isCI ? "fake-token-for-e2e-tests" : undefined, + gitHubToken: isCI ? "fake-token-for-e2e-tests" : undefined, }); onTestFinished(() => newClient.forceStop()); const session2 = await newClient.resumeSession(session.sessionId, { diff --git a/python/copilot/client.py b/python/copilot/client.py index a51940a96..658f66c82 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -1212,6 +1212,7 @@ async def create_session( commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, + github_token: str | None = None, ) -> CopilotSession: """ Create a new conversation session with the Copilot CLI. @@ -1344,6 +1345,10 @@ async def create_session( if hooks and any(hooks.values()): payload["hooks"] = True + # Add GitHub token for per-session authentication + if github_token is not None: + payload["gitHubToken"] = github_token + # Add working directory if provided if working_directory: payload["workingDirectory"] = working_directory @@ -1499,6 +1504,7 @@ async def resume_session( commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, create_session_fs_handler: CreateSessionFsHandler | None = None, + github_token: str | None = None, ) -> CopilotSession: """ Resume an existing conversation session by its ID. @@ -1642,6 +1648,10 @@ async def resume_session( if hooks and any(hooks.values()): payload["hooks"] = True + # Add GitHub token for per-session authentication + if github_token is not None: + payload["gitHubToken"] = github_token + if working_directory: payload["workingDirectory"] = working_directory if config_dir: diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 67c41fc96..2d07eb8a2 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -21,18 +21,6 @@ EnumT = TypeVar("EnumT", bound=Enum) -def from_int(x: Any) -> int: - assert isinstance(x, int) and not isinstance(x, bool) - return x - -def from_bool(x: Any) -> bool: - assert isinstance(x, bool) - return x - -def from_float(x: Any) -> float: - assert isinstance(x, (float, int)) and not isinstance(x, bool) - return float(x) - def from_str(x: Any) -> str: assert isinstance(x, str) return x @@ -49,6 +37,18 @@ def from_union(fs, x): pass assert False +def from_int(x: Any) -> int: + assert isinstance(x, int) and not isinstance(x, bool) + return x + +def from_bool(x: Any) -> bool: + assert isinstance(x, bool) + return x + +def from_float(x: Any) -> float: + assert isinstance(x, (float, int)) and not isinstance(x, bool) + return float(x) + def to_float(x: Any) -> float: assert isinstance(x, (int, float)) return x @@ -72,6 +72,25 @@ def to_enum(c: type[EnumT], x: Any) -> EnumT: def from_datetime(x: Any) -> datetime: return dateutil.parser.parse(x) +@dataclass +class AccountGetQuotaRequest: + git_hub_token: str | None = None + """GitHub token for per-user quota lookup. When provided, resolves this token to determine + the user's quota instead of using the global auth. + """ + + @staticmethod + def from_dict(obj: Any) -> 'AccountGetQuotaRequest': + assert isinstance(obj, dict) + git_hub_token = from_union([from_str, from_none], obj.get("gitHubToken")) + return AccountGetQuotaRequest(git_hub_token) + + def to_dict(self) -> dict: + result: dict = {} + if self.git_hub_token is not None: + result["gitHubToken"] = from_union([from_str, from_none], self.git_hub_token) + return result + @dataclass class AccountQuotaSnapshot: entitlement_requests: int @@ -169,6 +188,17 @@ def to_dict(self) -> dict: result["name"] = from_str(self.name) return result +class AuthInfoType(Enum): + """Authentication type""" + + API_KEY = "api-key" + COPILOT_API_TOKEN = "copilot-api-token" + ENV = "env" + GH_CLI = "gh-cli" + HMAC = "hmac" + TOKEN = "token" + USER = "user" + @dataclass class CommandsHandlePendingCommandRequest: request_id: str @@ -475,6 +505,43 @@ class MCPServerConfigType(Enum): SSE = "sse" STDIO = "stdio" +@dataclass +class MCPConfigDisableRequest: + names: list[str] + """Names of MCP servers to disable. Each server is added to the persisted disabled list so + new sessions skip it. Already-disabled names are ignored. Active sessions keep their + current connections until they end. + """ + + @staticmethod + def from_dict(obj: Any) -> 'MCPConfigDisableRequest': + assert isinstance(obj, dict) + names = from_list(from_str, obj.get("names")) + return MCPConfigDisableRequest(names) + + def to_dict(self) -> dict: + result: dict = {} + result["names"] = from_list(from_str, self.names) + return result + +@dataclass +class MCPConfigEnableRequest: + names: list[str] + """Names of MCP servers to enable. Each server is removed from the persisted disabled list + so new sessions spawn it. Unknown or already-enabled names are ignored. + """ + + @staticmethod + def from_dict(obj: Any) -> 'MCPConfigEnableRequest': + assert isinstance(obj, dict) + names = from_list(from_str, obj.get("names")) + return MCPConfigEnableRequest(names) + + def to_dict(self) -> dict: + result: dict = {} + result["names"] = from_list(from_str, self.names) + return result + @dataclass class MCPConfigRemoveRequest: name: str @@ -540,6 +607,71 @@ def to_dict(self) -> dict: result["serverName"] = from_str(self.server_name) return result +@dataclass +class MCPOauthLoginRequest: + server_name: str + """Name of the remote MCP server to authenticate""" + + callback_success_message: str | None = None + """Optional override for the body text shown on the OAuth loopback callback success page. + When omitted, the runtime applies a neutral fallback; callers driving interactive auth + should pass surface-specific copy telling the user where to return. + """ + client_name: str | None = None + """Optional override for the OAuth client display name shown on the consent screen. Applies + to newly registered dynamic clients only — existing registrations keep the name they were + created with. When omitted, the runtime applies a neutral fallback; callers driving + interactive auth should pass their own surface-specific label so the consent screen + matches the product the user sees. + """ + force_reauth: bool | None = None + """When true, clears any cached OAuth token for the server and runs a full new + authorization. Use when the user explicitly wants to switch accounts or believes their + session is stuck. + """ + + @staticmethod + def from_dict(obj: Any) -> 'MCPOauthLoginRequest': + assert isinstance(obj, dict) + server_name = from_str(obj.get("serverName")) + callback_success_message = from_union([from_str, from_none], obj.get("callbackSuccessMessage")) + client_name = from_union([from_str, from_none], obj.get("clientName")) + force_reauth = from_union([from_bool, from_none], obj.get("forceReauth")) + return MCPOauthLoginRequest(server_name, callback_success_message, client_name, force_reauth) + + def to_dict(self) -> dict: + result: dict = {} + result["serverName"] = from_str(self.server_name) + if self.callback_success_message is not None: + result["callbackSuccessMessage"] = from_union([from_str, from_none], self.callback_success_message) + if self.client_name is not None: + result["clientName"] = from_union([from_str, from_none], self.client_name) + if self.force_reauth is not None: + result["forceReauth"] = from_union([from_bool, from_none], self.force_reauth) + return result + +@dataclass +class MCPOauthLoginResult: + authorization_url: str | None = None + """URL the caller should open in a browser to complete OAuth. Omitted when cached tokens + were still valid and no browser interaction was needed — the server is already + reconnected in that case. When present, the runtime starts the callback listener before + returning and continues the flow in the background; completion is signaled via + session.mcp_server_status_changed. + """ + + @staticmethod + def from_dict(obj: Any) -> 'MCPOauthLoginResult': + assert isinstance(obj, dict) + authorization_url = from_union([from_str, from_none], obj.get("authorizationUrl")) + return MCPOauthLoginResult(authorization_url) + + def to_dict(self) -> dict: + result: dict = {} + if self.authorization_url is not None: + result["authorizationUrl"] = from_union([from_str, from_none], self.authorization_url) + return result + class MCPServerStatus(Enum): """Connection status: connected, failed, needs-auth, pending, disabled, or not_configured""" @@ -645,20 +777,21 @@ class ModelPolicy: state: str """Current policy state for this model""" - terms: str + terms: str | None = None """Usage terms or conditions for this model""" @staticmethod def from_dict(obj: Any) -> 'ModelPolicy': assert isinstance(obj, dict) state = from_str(obj.get("state")) - terms = from_str(obj.get("terms")) + terms = from_union([from_str, from_none], obj.get("terms")) return ModelPolicy(state, terms) def to_dict(self) -> dict: result: dict = {} result["state"] = from_str(self.state) - result["terms"] = from_str(self.terms) + if self.terms is not None: + result["terms"] = from_union([from_str, from_none], self.terms) return result @dataclass @@ -729,6 +862,25 @@ def to_dict(self) -> dict: result["modelId"] = from_union([from_str, from_none], self.model_id) return result +@dataclass +class ModelsListRequest: + git_hub_token: str | None = None + """GitHub token for per-user model listing. When provided, resolves this token to determine + the user's Copilot plan and available models instead of using the global auth. + """ + + @staticmethod + def from_dict(obj: Any) -> 'ModelsListRequest': + assert isinstance(obj, dict) + git_hub_token = from_union([from_str, from_none], obj.get("gitHubToken")) + return ModelsListRequest(git_hub_token) + + def to_dict(self) -> dict: + result: dict = {} + if self.git_hub_token is not None: + result["gitHubToken"] = from_union([from_str, from_none], self.git_hub_token) + return result + @dataclass class NameGetResult: name: str | None = None @@ -761,8 +913,18 @@ def to_dict(self) -> dict: result["name"] = from_str(self.name) return result +class ApprovalKind(Enum): + COMMANDS = "commands" + CUSTOM_TOOL = "custom-tool" + MCP = "mcp" + MCP_SAMPLING = "mcp-sampling" + MEMORY = "memory" + WRITE = "write" + class PermissionDecisionKind(Enum): APPROVED = "approved" + APPROVED_FOR_LOCATION = "approved-for-location" + APPROVED_FOR_SESSION = "approved-for-session" DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy" DENIED_BY_PERMISSION_REQUEST_HOOK = "denied-by-permission-request-hook" DENIED_BY_RULES = "denied-by-rules" @@ -772,6 +934,30 @@ class PermissionDecisionKind(Enum): class PermissionDecisionApprovedKind(Enum): APPROVED = "approved" +class PermissionDecisionApprovedForLocationKind(Enum): + APPROVED_FOR_LOCATION = "approved-for-location" + +class PermissionDecisionApprovedForLocationApprovalCommandsKind(Enum): + COMMANDS = "commands" + +class PermissionDecisionApprovedForLocationApprovalCustomToolKind(Enum): + CUSTOM_TOOL = "custom-tool" + +class PermissionDecisionApprovedForLocationApprovalMCPKind(Enum): + MCP = "mcp" + +class PermissionDecisionApprovedForLocationApprovalMCPSamplingKind(Enum): + MCP_SAMPLING = "mcp-sampling" + +class PermissionDecisionApprovedForLocationApprovalMemoryKind(Enum): + MEMORY = "memory" + +class PermissionDecisionApprovedForLocationApprovalWriteKind(Enum): + WRITE = "write" + +class PermissionDecisionApprovedForSessionKind(Enum): + APPROVED_FOR_SESSION = "approved-for-session" + class PermissionDecisionDeniedByContentExclusionPolicyKind(Enum): DENIED_BY_CONTENT_EXCLUSION_POLICY = "denied-by-content-exclusion-policy" @@ -803,6 +989,65 @@ def to_dict(self) -> dict: result["success"] = from_bool(self.success) return result +@dataclass +class PermissionsResetSessionApprovalsRequest: + @staticmethod + def from_dict(obj: Any) -> 'PermissionsResetSessionApprovalsRequest': + assert isinstance(obj, dict) + return PermissionsResetSessionApprovalsRequest() + + def to_dict(self) -> dict: + result: dict = {} + return result + +@dataclass +class PermissionsResetSessionApprovalsResult: + success: bool + """Whether the operation succeeded""" + + @staticmethod + def from_dict(obj: Any) -> 'PermissionsResetSessionApprovalsResult': + assert isinstance(obj, dict) + success = from_bool(obj.get("success")) + return PermissionsResetSessionApprovalsResult(success) + + def to_dict(self) -> dict: + result: dict = {} + result["success"] = from_bool(self.success) + return result + +@dataclass +class PermissionsSetApproveAllRequest: + enabled: bool + """Whether to auto-approve all tool permission requests""" + + @staticmethod + def from_dict(obj: Any) -> 'PermissionsSetApproveAllRequest': + assert isinstance(obj, dict) + enabled = from_bool(obj.get("enabled")) + return PermissionsSetApproveAllRequest(enabled) + + def to_dict(self) -> dict: + result: dict = {} + result["enabled"] = from_bool(self.enabled) + return result + +@dataclass +class PermissionsSetApproveAllResult: + success: bool + """Whether the operation succeeded""" + + @staticmethod + def from_dict(obj: Any) -> 'PermissionsSetApproveAllResult': + assert isinstance(obj, dict) + success = from_bool(obj.get("success")) + return PermissionsSetApproveAllResult(success) + + def to_dict(self) -> dict: + result: dict = {} + result["success"] = from_bool(self.success) + return result + @dataclass class PingRequest: message: str | None = None @@ -1939,6 +2184,52 @@ def to_dict(self) -> dict: result["agent"] = to_class(AgentInfo, self.agent) return result +@dataclass +class SessionAuthStatus: + is_authenticated: bool + """Whether the session has resolved authentication""" + + auth_type: AuthInfoType | None = None + """Authentication type""" + + copilot_plan: str | None = None + """Copilot plan tier (e.g., individual_pro, business)""" + + host: str | None = None + """Authentication host URL""" + + login: str | None = None + """Authenticated login/username, if available""" + + status_message: str | None = None + """Human-readable authentication status description""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionAuthStatus': + assert isinstance(obj, dict) + is_authenticated = from_bool(obj.get("isAuthenticated")) + auth_type = from_union([AuthInfoType, from_none], obj.get("authType")) + copilot_plan = from_union([from_str, from_none], obj.get("copilotPlan")) + host = from_union([from_str, from_none], obj.get("host")) + login = from_union([from_str, from_none], obj.get("login")) + status_message = from_union([from_str, from_none], obj.get("statusMessage")) + return SessionAuthStatus(is_authenticated, auth_type, copilot_plan, host, login, status_message) + + def to_dict(self) -> dict: + result: dict = {} + result["isAuthenticated"] = from_bool(self.is_authenticated) + if self.auth_type is not None: + result["authType"] = from_union([lambda x: to_enum(AuthInfoType, x), from_none], self.auth_type) + if self.copilot_plan is not None: + result["copilotPlan"] = from_union([from_str, from_none], self.copilot_plan) + if self.host is not None: + result["host"] = from_union([from_str, from_none], self.host) + if self.login is not None: + result["login"] = from_union([from_str, from_none], self.login) + if self.status_message is not None: + result["statusMessage"] = from_union([from_str, from_none], self.status_message) + return result + @dataclass class DiscoveredMCPServer: enabled: bool @@ -2422,61 +2713,92 @@ def to_dict(self) -> dict: return result @dataclass -class PermissionDecision: - kind: PermissionDecisionKind - """The permission request was approved +class PermissionDecisionApprovedForIonApproval: + """The approval to add as a session-scoped rule - Denied because approval rules explicitly blocked it + The approval to persist for this location + """ + kind: ApprovalKind + command_identifiers: list[str] | None = None + server_name: str | None = None + tool_name: str | None = None - Denied because no approval rule matched and user confirmation was unavailable + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForIonApproval': + assert isinstance(obj, dict) + kind = ApprovalKind(obj.get("kind")) + command_identifiers = from_union([lambda x: from_list(from_str, x), from_none], obj.get("commandIdentifiers")) + server_name = from_union([from_str, from_none], obj.get("serverName")) + tool_name = from_union([from_none, from_str], obj.get("toolName")) + return PermissionDecisionApprovedForIonApproval(kind, command_identifiers, server_name, tool_name) - Denied by the user during an interactive prompt + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(ApprovalKind, self.kind) + if self.command_identifiers is not None: + result["commandIdentifiers"] = from_union([lambda x: from_list(from_str, x), from_none], self.command_identifiers) + if self.server_name is not None: + result["serverName"] = from_union([from_str, from_none], self.server_name) + if self.tool_name is not None: + result["toolName"] = from_union([from_none, from_str], self.tool_name) + return result - Denied by the organization's content exclusion policy +@dataclass +class PermissionDecisionApprovedForLocationApproval: + """The approval to persist for this location""" - Denied by a permission request hook registered by an extension or plugin - """ - rules: list[Any] | None = None - """Rules that denied the request""" + kind: ApprovalKind + command_identifiers: list[str] | None = None + server_name: str | None = None + tool_name: str | None = None - feedback: str | None = None - """Optional feedback from the user explaining the denial""" + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForLocationApproval': + assert isinstance(obj, dict) + kind = ApprovalKind(obj.get("kind")) + command_identifiers = from_union([lambda x: from_list(from_str, x), from_none], obj.get("commandIdentifiers")) + server_name = from_union([from_str, from_none], obj.get("serverName")) + tool_name = from_union([from_none, from_str], obj.get("toolName")) + return PermissionDecisionApprovedForLocationApproval(kind, command_identifiers, server_name, tool_name) - message: str | None = None - """Human-readable explanation of why the path was excluded + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(ApprovalKind, self.kind) + if self.command_identifiers is not None: + result["commandIdentifiers"] = from_union([lambda x: from_list(from_str, x), from_none], self.command_identifiers) + if self.server_name is not None: + result["serverName"] = from_union([from_str, from_none], self.server_name) + if self.tool_name is not None: + result["toolName"] = from_union([from_none, from_str], self.tool_name) + return result - Optional message from the hook explaining the denial - """ - path: str | None = None - """File path that triggered the exclusion""" +@dataclass +class PermissionDecisionApprovedForSessionApproval: + """The approval to add as a session-scoped rule""" - interrupt: bool | None = None - """Whether to interrupt the current agent turn""" + kind: ApprovalKind + command_identifiers: list[str] | None = None + server_name: str | None = None + tool_name: str | None = None @staticmethod - def from_dict(obj: Any) -> 'PermissionDecision': + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForSessionApproval': assert isinstance(obj, dict) - kind = PermissionDecisionKind(obj.get("kind")) - rules = from_union([lambda x: from_list(lambda x: x, x), from_none], obj.get("rules")) - feedback = from_union([from_str, from_none], obj.get("feedback")) - message = from_union([from_str, from_none], obj.get("message")) - path = from_union([from_str, from_none], obj.get("path")) - interrupt = from_union([from_bool, from_none], obj.get("interrupt")) - return PermissionDecision(kind, rules, feedback, message, path, interrupt) + kind = ApprovalKind(obj.get("kind")) + command_identifiers = from_union([lambda x: from_list(from_str, x), from_none], obj.get("commandIdentifiers")) + server_name = from_union([from_str, from_none], obj.get("serverName")) + tool_name = from_union([from_none, from_str], obj.get("toolName")) + return PermissionDecisionApprovedForSessionApproval(kind, command_identifiers, server_name, tool_name) def to_dict(self) -> dict: result: dict = {} - result["kind"] = to_enum(PermissionDecisionKind, self.kind) - if self.rules is not None: - result["rules"] = from_union([lambda x: from_list(lambda x: x, x), from_none], self.rules) - if self.feedback is not None: - result["feedback"] = from_union([from_str, from_none], self.feedback) - if self.message is not None: - result["message"] = from_union([from_str, from_none], self.message) - if self.path is not None: - result["path"] = from_union([from_str, from_none], self.path) - if self.interrupt is not None: - result["interrupt"] = from_union([from_bool, from_none], self.interrupt) + result["kind"] = to_enum(ApprovalKind, self.kind) + if self.command_identifiers is not None: + result["commandIdentifiers"] = from_union([lambda x: from_list(from_str, x), from_none], self.command_identifiers) + if self.server_name is not None: + result["serverName"] = from_union([from_str, from_none], self.server_name) + if self.tool_name is not None: + result["toolName"] = from_union([from_none, from_str], self.tool_name) return result @dataclass @@ -2495,6 +2817,216 @@ def to_dict(self) -> dict: result["kind"] = to_enum(PermissionDecisionApprovedKind, self.kind) return result +@dataclass +class PermissionDecisionApprovedForLocationApprovalCommands: + command_identifiers: list[str] + kind: PermissionDecisionApprovedForLocationApprovalCommandsKind + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForLocationApprovalCommands': + assert isinstance(obj, dict) + command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) + kind = PermissionDecisionApprovedForLocationApprovalCommandsKind(obj.get("kind")) + return PermissionDecisionApprovedForLocationApprovalCommands(command_identifiers, kind) + + def to_dict(self) -> dict: + result: dict = {} + result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalCommandsKind, self.kind) + return result + +@dataclass +class PermissionDecisionApprovedForSessionApprovalCommands: + command_identifiers: list[str] + kind: PermissionDecisionApprovedForLocationApprovalCommandsKind + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForSessionApprovalCommands': + assert isinstance(obj, dict) + command_identifiers = from_list(from_str, obj.get("commandIdentifiers")) + kind = PermissionDecisionApprovedForLocationApprovalCommandsKind(obj.get("kind")) + return PermissionDecisionApprovedForSessionApprovalCommands(command_identifiers, kind) + + def to_dict(self) -> dict: + result: dict = {} + result["commandIdentifiers"] = from_list(from_str, self.command_identifiers) + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalCommandsKind, self.kind) + return result + +@dataclass +class PermissionDecisionApprovedForLocationApprovalCustomTool: + kind: PermissionDecisionApprovedForLocationApprovalCustomToolKind + tool_name: str + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForLocationApprovalCustomTool': + assert isinstance(obj, dict) + kind = PermissionDecisionApprovedForLocationApprovalCustomToolKind(obj.get("kind")) + tool_name = from_str(obj.get("toolName")) + return PermissionDecisionApprovedForLocationApprovalCustomTool(kind, tool_name) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalCustomToolKind, self.kind) + result["toolName"] = from_str(self.tool_name) + return result + +@dataclass +class PermissionDecisionApprovedForSessionApprovalCustomTool: + kind: PermissionDecisionApprovedForLocationApprovalCustomToolKind + tool_name: str + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForSessionApprovalCustomTool': + assert isinstance(obj, dict) + kind = PermissionDecisionApprovedForLocationApprovalCustomToolKind(obj.get("kind")) + tool_name = from_str(obj.get("toolName")) + return PermissionDecisionApprovedForSessionApprovalCustomTool(kind, tool_name) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalCustomToolKind, self.kind) + result["toolName"] = from_str(self.tool_name) + return result + +@dataclass +class PermissionDecisionApprovedForLocationApprovalMCP: + kind: PermissionDecisionApprovedForLocationApprovalMCPKind + server_name: str + tool_name: str | None = None + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForLocationApprovalMCP': + assert isinstance(obj, dict) + kind = PermissionDecisionApprovedForLocationApprovalMCPKind(obj.get("kind")) + server_name = from_str(obj.get("serverName")) + tool_name = from_union([from_none, from_str], obj.get("toolName")) + return PermissionDecisionApprovedForLocationApprovalMCP(kind, server_name, tool_name) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalMCPKind, self.kind) + result["serverName"] = from_str(self.server_name) + result["toolName"] = from_union([from_none, from_str], self.tool_name) + return result + +@dataclass +class PermissionDecisionApprovedForSessionApprovalMCP: + kind: PermissionDecisionApprovedForLocationApprovalMCPKind + server_name: str + tool_name: str | None = None + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForSessionApprovalMCP': + assert isinstance(obj, dict) + kind = PermissionDecisionApprovedForLocationApprovalMCPKind(obj.get("kind")) + server_name = from_str(obj.get("serverName")) + tool_name = from_union([from_none, from_str], obj.get("toolName")) + return PermissionDecisionApprovedForSessionApprovalMCP(kind, server_name, tool_name) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalMCPKind, self.kind) + result["serverName"] = from_str(self.server_name) + result["toolName"] = from_union([from_none, from_str], self.tool_name) + return result + +@dataclass +class PermissionDecisionApprovedForLocationApprovalMCPSampling: + kind: PermissionDecisionApprovedForLocationApprovalMCPSamplingKind + server_name: str + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForLocationApprovalMCPSampling': + assert isinstance(obj, dict) + kind = PermissionDecisionApprovedForLocationApprovalMCPSamplingKind(obj.get("kind")) + server_name = from_str(obj.get("serverName")) + return PermissionDecisionApprovedForLocationApprovalMCPSampling(kind, server_name) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalMCPSamplingKind, self.kind) + result["serverName"] = from_str(self.server_name) + return result + +@dataclass +class PermissionDecisionApprovedForSessionApprovalMCPSampling: + kind: PermissionDecisionApprovedForLocationApprovalMCPSamplingKind + server_name: str + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForSessionApprovalMCPSampling': + assert isinstance(obj, dict) + kind = PermissionDecisionApprovedForLocationApprovalMCPSamplingKind(obj.get("kind")) + server_name = from_str(obj.get("serverName")) + return PermissionDecisionApprovedForSessionApprovalMCPSampling(kind, server_name) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalMCPSamplingKind, self.kind) + result["serverName"] = from_str(self.server_name) + return result + +@dataclass +class PermissionDecisionApprovedForLocationApprovalMemory: + kind: PermissionDecisionApprovedForLocationApprovalMemoryKind + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForLocationApprovalMemory': + assert isinstance(obj, dict) + kind = PermissionDecisionApprovedForLocationApprovalMemoryKind(obj.get("kind")) + return PermissionDecisionApprovedForLocationApprovalMemory(kind) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalMemoryKind, self.kind) + return result + +@dataclass +class PermissionDecisionApprovedForSessionApprovalMemory: + kind: PermissionDecisionApprovedForLocationApprovalMemoryKind + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForSessionApprovalMemory': + assert isinstance(obj, dict) + kind = PermissionDecisionApprovedForLocationApprovalMemoryKind(obj.get("kind")) + return PermissionDecisionApprovedForSessionApprovalMemory(kind) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalMemoryKind, self.kind) + return result + +@dataclass +class PermissionDecisionApprovedForLocationApprovalWrite: + kind: PermissionDecisionApprovedForLocationApprovalWriteKind + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForLocationApprovalWrite': + assert isinstance(obj, dict) + kind = PermissionDecisionApprovedForLocationApprovalWriteKind(obj.get("kind")) + return PermissionDecisionApprovedForLocationApprovalWrite(kind) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalWriteKind, self.kind) + return result + +@dataclass +class PermissionDecisionApprovedForSessionApprovalWrite: + kind: PermissionDecisionApprovedForLocationApprovalWriteKind + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForSessionApprovalWrite': + assert isinstance(obj, dict) + kind = PermissionDecisionApprovedForLocationApprovalWriteKind(obj.get("kind")) + return PermissionDecisionApprovedForSessionApprovalWrite(kind) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(PermissionDecisionApprovedForLocationApprovalWriteKind, self.kind) + return result + @dataclass class PermissionDecisionDeniedByContentExclusionPolicy: kind: PermissionDecisionDeniedByContentExclusionPolicyKind @@ -3290,23 +3822,126 @@ def to_dict(self) -> dict: return result @dataclass -class PermissionDecisionRequest: - request_id: str - """Request ID of the pending permission request""" +class PermissionDecision: + kind: PermissionDecisionKind + """The permission request was approved - result: PermissionDecision + Approved and remembered for the rest of the session + + Approved and persisted for this project location + + Denied because approval rules explicitly blocked it + + Denied because no approval rule matched and user confirmation was unavailable + + Denied by the user during an interactive prompt + + Denied by the organization's content exclusion policy + + Denied by a permission request hook registered by an extension or plugin + """ + approval: PermissionDecisionApprovedForIonApproval | None = None + """The approval to add as a session-scoped rule + + The approval to persist for this location + """ + location_key: str | None = None + """The location key (git root or cwd) to persist the approval to""" + + rules: list[Any] | None = None + """Rules that denied the request""" + + feedback: str | None = None + """Optional feedback from the user explaining the denial""" + + message: str | None = None + """Human-readable explanation of why the path was excluded + + Optional message from the hook explaining the denial + """ + path: str | None = None + """File path that triggered the exclusion""" + + interrupt: bool | None = None + """Whether to interrupt the current agent turn""" @staticmethod - def from_dict(obj: Any) -> 'PermissionDecisionRequest': + def from_dict(obj: Any) -> 'PermissionDecision': assert isinstance(obj, dict) - request_id = from_str(obj.get("requestId")) - result = PermissionDecision.from_dict(obj.get("result")) - return PermissionDecisionRequest(request_id, result) + kind = PermissionDecisionKind(obj.get("kind")) + approval = from_union([PermissionDecisionApprovedForIonApproval.from_dict, from_none], obj.get("approval")) + location_key = from_union([from_str, from_none], obj.get("locationKey")) + rules = from_union([lambda x: from_list(lambda x: x, x), from_none], obj.get("rules")) + feedback = from_union([from_str, from_none], obj.get("feedback")) + message = from_union([from_str, from_none], obj.get("message")) + path = from_union([from_str, from_none], obj.get("path")) + interrupt = from_union([from_bool, from_none], obj.get("interrupt")) + return PermissionDecision(kind, approval, location_key, rules, feedback, message, path, interrupt) def to_dict(self) -> dict: result: dict = {} - result["requestId"] = from_str(self.request_id) - result["result"] = to_class(PermissionDecision, self.result) + result["kind"] = to_enum(PermissionDecisionKind, self.kind) + if self.approval is not None: + result["approval"] = from_union([lambda x: to_class(PermissionDecisionApprovedForIonApproval, x), from_none], self.approval) + if self.location_key is not None: + result["locationKey"] = from_union([from_str, from_none], self.location_key) + if self.rules is not None: + result["rules"] = from_union([lambda x: from_list(lambda x: x, x), from_none], self.rules) + if self.feedback is not None: + result["feedback"] = from_union([from_str, from_none], self.feedback) + if self.message is not None: + result["message"] = from_union([from_str, from_none], self.message) + if self.path is not None: + result["path"] = from_union([from_str, from_none], self.path) + if self.interrupt is not None: + result["interrupt"] = from_union([from_bool, from_none], self.interrupt) + return result + +@dataclass +class PermissionDecisionApprovedForLocation: + approval: PermissionDecisionApprovedForLocationApproval + """The approval to persist for this location""" + + kind: PermissionDecisionApprovedForLocationKind + """Approved and persisted for this project location""" + + location_key: str + """The location key (git root or cwd) to persist the approval to""" + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForLocation': + assert isinstance(obj, dict) + approval = PermissionDecisionApprovedForLocationApproval.from_dict(obj.get("approval")) + kind = PermissionDecisionApprovedForLocationKind(obj.get("kind")) + location_key = from_str(obj.get("locationKey")) + return PermissionDecisionApprovedForLocation(approval, kind, location_key) + + def to_dict(self) -> dict: + result: dict = {} + result["approval"] = to_class(PermissionDecisionApprovedForLocationApproval, self.approval) + result["kind"] = to_enum(PermissionDecisionApprovedForLocationKind, self.kind) + result["locationKey"] = from_str(self.location_key) + return result + +@dataclass +class PermissionDecisionApprovedForSession: + approval: PermissionDecisionApprovedForSessionApproval + """The approval to add as a session-scoped rule""" + + kind: PermissionDecisionApprovedForSessionKind + """Approved and remembered for the rest of the session""" + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionApprovedForSession': + assert isinstance(obj, dict) + approval = PermissionDecisionApprovedForSessionApproval.from_dict(obj.get("approval")) + kind = PermissionDecisionApprovedForSessionKind(obj.get("kind")) + return PermissionDecisionApprovedForSession(approval, kind) + + def to_dict(self) -> dict: + result: dict = {} + result["approval"] = to_class(PermissionDecisionApprovedForSessionApproval, self.approval) + result["kind"] = to_enum(PermissionDecisionApprovedForSessionKind, self.kind) return result @dataclass @@ -3660,6 +4295,26 @@ def to_dict(self) -> dict: result["workspace"] = from_union([lambda x: to_class(Workspace, x), from_none], self.workspace) return result +@dataclass +class PermissionDecisionRequest: + request_id: str + """Request ID of the pending permission request""" + + result: PermissionDecision + + @staticmethod + def from_dict(obj: Any) -> 'PermissionDecisionRequest': + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + result = PermissionDecision.from_dict(obj.get("result")) + return PermissionDecisionRequest(request_id, result) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["result"] = to_class(PermissionDecision, self.result) + return result + @dataclass class UIElicitationSchema: """JSON Schema describing the form fields to present to the user""" @@ -3831,6 +4486,7 @@ def to_dict(self) -> dict: @dataclass class RPC: + account_get_quota_request: AccountGetQuotaRequest account_get_quota_result: AccountGetQuotaResult account_quota_snapshot: AccountQuotaSnapshot agent_get_current_result: AgentGetCurrentResult @@ -3839,6 +4495,7 @@ class RPC: agent_reload_result: AgentReloadResult agent_select_request: AgentSelectRequest agent_select_result: AgentSelectResult + auth_info_type: AuthInfoType commands_handle_pending_command_request: CommandsHandlePendingCommandRequest commands_handle_pending_command_result: CommandsHandlePendingCommandResult current_model: CurrentModel @@ -3868,6 +4525,8 @@ class RPC: log_request: LogRequest log_result: LogResult mcp_config_add_request: MCPConfigAddRequest + mcp_config_disable_request: MCPConfigDisableRequest + mcp_config_enable_request: MCPConfigEnableRequest mcp_config_list: MCPConfigList mcp_config_remove_request: MCPConfigRemoveRequest mcp_config_update_request: MCPConfigUpdateRequest @@ -3875,6 +4534,8 @@ class RPC: mcp_discover_request: MCPDiscoverRequest mcp_discover_result: MCPDiscoverResult mcp_enable_request: MCPEnableRequest + mcp_oauth_login_request: MCPOauthLoginRequest + mcp_oauth_login_result: MCPOauthLoginResult mcp_server: MCPServer mcp_server_config: MCPServerConfig mcp_server_config_http: MCPServerConfigHTTP @@ -3896,6 +4557,7 @@ class RPC: model_capabilities_supports: ModelCapabilitiesSupports model_list: ModelList model_policy: ModelPolicy + models_list_request: ModelsListRequest model_switch_to_request: ModelSwitchToRequest model_switch_to_result: ModelSwitchToResult mode_set_request: ModeSetRequest @@ -3903,6 +4565,22 @@ class RPC: name_set_request: NameSetRequest permission_decision: PermissionDecision permission_decision_approved: PermissionDecisionApproved + permission_decision_approved_for_location: PermissionDecisionApprovedForLocation + permission_decision_approved_for_location_approval: PermissionDecisionApprovedForLocationApproval + permission_decision_approved_for_location_approval_commands: PermissionDecisionApprovedForLocationApprovalCommands + permission_decision_approved_for_location_approval_custom_tool: PermissionDecisionApprovedForLocationApprovalCustomTool + permission_decision_approved_for_location_approval_mcp: PermissionDecisionApprovedForLocationApprovalMCP + permission_decision_approved_for_location_approval_mcp_sampling: PermissionDecisionApprovedForLocationApprovalMCPSampling + permission_decision_approved_for_location_approval_memory: PermissionDecisionApprovedForLocationApprovalMemory + permission_decision_approved_for_location_approval_write: PermissionDecisionApprovedForLocationApprovalWrite + permission_decision_approved_for_session: PermissionDecisionApprovedForSession + permission_decision_approved_for_session_approval: PermissionDecisionApprovedForSessionApproval + permission_decision_approved_for_session_approval_commands: PermissionDecisionApprovedForSessionApprovalCommands + permission_decision_approved_for_session_approval_custom_tool: PermissionDecisionApprovedForSessionApprovalCustomTool + permission_decision_approved_for_session_approval_mcp: PermissionDecisionApprovedForSessionApprovalMCP + permission_decision_approved_for_session_approval_mcp_sampling: PermissionDecisionApprovedForSessionApprovalMCPSampling + permission_decision_approved_for_session_approval_memory: PermissionDecisionApprovedForSessionApprovalMemory + permission_decision_approved_for_session_approval_write: PermissionDecisionApprovedForSessionApprovalWrite permission_decision_denied_by_content_exclusion_policy: PermissionDecisionDeniedByContentExclusionPolicy permission_decision_denied_by_permission_request_hook: PermissionDecisionDeniedByPermissionRequestHook permission_decision_denied_by_rules: PermissionDecisionDeniedByRules @@ -3910,6 +4588,10 @@ class RPC: permission_decision_denied_no_approval_rule_and_could_not_request_from_user: PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser permission_decision_request: PermissionDecisionRequest permission_request_result: PermissionRequestResult + permissions_reset_session_approvals_request: PermissionsResetSessionApprovalsRequest + permissions_reset_session_approvals_result: PermissionsResetSessionApprovalsResult + permissions_set_approve_all_request: PermissionsSetApproveAllRequest + permissions_set_approve_all_result: PermissionsSetApproveAllResult ping_request: PingRequest ping_result: PingResult plan_read_result: PlanReadResult @@ -3918,6 +4600,7 @@ class RPC: plugin_list: PluginList server_skill: ServerSkill server_skill_list: ServerSkillList + session_auth_status: SessionAuthStatus session_fs_append_file_request: SessionFSAppendFileRequest session_fs_error: SessionFSError session_fs_error_code: SessionFSErrorCode @@ -3997,6 +4680,7 @@ class RPC: @staticmethod def from_dict(obj: Any) -> 'RPC': assert isinstance(obj, dict) + account_get_quota_request = AccountGetQuotaRequest.from_dict(obj.get("AccountGetQuotaRequest")) account_get_quota_result = AccountGetQuotaResult.from_dict(obj.get("AccountGetQuotaResult")) account_quota_snapshot = AccountQuotaSnapshot.from_dict(obj.get("AccountQuotaSnapshot")) agent_get_current_result = AgentGetCurrentResult.from_dict(obj.get("AgentGetCurrentResult")) @@ -4005,6 +4689,7 @@ def from_dict(obj: Any) -> 'RPC': agent_reload_result = AgentReloadResult.from_dict(obj.get("AgentReloadResult")) agent_select_request = AgentSelectRequest.from_dict(obj.get("AgentSelectRequest")) agent_select_result = AgentSelectResult.from_dict(obj.get("AgentSelectResult")) + auth_info_type = AuthInfoType(obj.get("AuthInfoType")) commands_handle_pending_command_request = CommandsHandlePendingCommandRequest.from_dict(obj.get("CommandsHandlePendingCommandRequest")) commands_handle_pending_command_result = CommandsHandlePendingCommandResult.from_dict(obj.get("CommandsHandlePendingCommandResult")) current_model = CurrentModel.from_dict(obj.get("CurrentModel")) @@ -4034,6 +4719,8 @@ def from_dict(obj: Any) -> 'RPC': log_request = LogRequest.from_dict(obj.get("LogRequest")) log_result = LogResult.from_dict(obj.get("LogResult")) mcp_config_add_request = MCPConfigAddRequest.from_dict(obj.get("McpConfigAddRequest")) + mcp_config_disable_request = MCPConfigDisableRequest.from_dict(obj.get("McpConfigDisableRequest")) + mcp_config_enable_request = MCPConfigEnableRequest.from_dict(obj.get("McpConfigEnableRequest")) mcp_config_list = MCPConfigList.from_dict(obj.get("McpConfigList")) mcp_config_remove_request = MCPConfigRemoveRequest.from_dict(obj.get("McpConfigRemoveRequest")) mcp_config_update_request = MCPConfigUpdateRequest.from_dict(obj.get("McpConfigUpdateRequest")) @@ -4041,6 +4728,8 @@ def from_dict(obj: Any) -> 'RPC': mcp_discover_request = MCPDiscoverRequest.from_dict(obj.get("McpDiscoverRequest")) mcp_discover_result = MCPDiscoverResult.from_dict(obj.get("McpDiscoverResult")) mcp_enable_request = MCPEnableRequest.from_dict(obj.get("McpEnableRequest")) + mcp_oauth_login_request = MCPOauthLoginRequest.from_dict(obj.get("McpOauthLoginRequest")) + mcp_oauth_login_result = MCPOauthLoginResult.from_dict(obj.get("McpOauthLoginResult")) mcp_server = MCPServer.from_dict(obj.get("McpServer")) mcp_server_config = MCPServerConfig.from_dict(obj.get("McpServerConfig")) mcp_server_config_http = MCPServerConfigHTTP.from_dict(obj.get("McpServerConfigHttp")) @@ -4062,6 +4751,7 @@ def from_dict(obj: Any) -> 'RPC': model_capabilities_supports = ModelCapabilitiesSupports.from_dict(obj.get("ModelCapabilitiesSupports")) model_list = ModelList.from_dict(obj.get("ModelList")) model_policy = ModelPolicy.from_dict(obj.get("ModelPolicy")) + models_list_request = ModelsListRequest.from_dict(obj.get("ModelsListRequest")) model_switch_to_request = ModelSwitchToRequest.from_dict(obj.get("ModelSwitchToRequest")) model_switch_to_result = ModelSwitchToResult.from_dict(obj.get("ModelSwitchToResult")) mode_set_request = ModeSetRequest.from_dict(obj.get("ModeSetRequest")) @@ -4069,6 +4759,22 @@ def from_dict(obj: Any) -> 'RPC': name_set_request = NameSetRequest.from_dict(obj.get("NameSetRequest")) permission_decision = PermissionDecision.from_dict(obj.get("PermissionDecision")) permission_decision_approved = PermissionDecisionApproved.from_dict(obj.get("PermissionDecisionApproved")) + permission_decision_approved_for_location = PermissionDecisionApprovedForLocation.from_dict(obj.get("PermissionDecisionApprovedForLocation")) + permission_decision_approved_for_location_approval = PermissionDecisionApprovedForLocationApproval.from_dict(obj.get("PermissionDecisionApprovedForLocationApproval")) + permission_decision_approved_for_location_approval_commands = PermissionDecisionApprovedForLocationApprovalCommands.from_dict(obj.get("PermissionDecisionApprovedForLocationApprovalCommands")) + permission_decision_approved_for_location_approval_custom_tool = PermissionDecisionApprovedForLocationApprovalCustomTool.from_dict(obj.get("PermissionDecisionApprovedForLocationApprovalCustomTool")) + permission_decision_approved_for_location_approval_mcp = PermissionDecisionApprovedForLocationApprovalMCP.from_dict(obj.get("PermissionDecisionApprovedForLocationApprovalMcp")) + permission_decision_approved_for_location_approval_mcp_sampling = PermissionDecisionApprovedForLocationApprovalMCPSampling.from_dict(obj.get("PermissionDecisionApprovedForLocationApprovalMcpSampling")) + permission_decision_approved_for_location_approval_memory = PermissionDecisionApprovedForLocationApprovalMemory.from_dict(obj.get("PermissionDecisionApprovedForLocationApprovalMemory")) + permission_decision_approved_for_location_approval_write = PermissionDecisionApprovedForLocationApprovalWrite.from_dict(obj.get("PermissionDecisionApprovedForLocationApprovalWrite")) + permission_decision_approved_for_session = PermissionDecisionApprovedForSession.from_dict(obj.get("PermissionDecisionApprovedForSession")) + permission_decision_approved_for_session_approval = PermissionDecisionApprovedForSessionApproval.from_dict(obj.get("PermissionDecisionApprovedForSessionApproval")) + permission_decision_approved_for_session_approval_commands = PermissionDecisionApprovedForSessionApprovalCommands.from_dict(obj.get("PermissionDecisionApprovedForSessionApprovalCommands")) + permission_decision_approved_for_session_approval_custom_tool = PermissionDecisionApprovedForSessionApprovalCustomTool.from_dict(obj.get("PermissionDecisionApprovedForSessionApprovalCustomTool")) + permission_decision_approved_for_session_approval_mcp = PermissionDecisionApprovedForSessionApprovalMCP.from_dict(obj.get("PermissionDecisionApprovedForSessionApprovalMcp")) + permission_decision_approved_for_session_approval_mcp_sampling = PermissionDecisionApprovedForSessionApprovalMCPSampling.from_dict(obj.get("PermissionDecisionApprovedForSessionApprovalMcpSampling")) + permission_decision_approved_for_session_approval_memory = PermissionDecisionApprovedForSessionApprovalMemory.from_dict(obj.get("PermissionDecisionApprovedForSessionApprovalMemory")) + permission_decision_approved_for_session_approval_write = PermissionDecisionApprovedForSessionApprovalWrite.from_dict(obj.get("PermissionDecisionApprovedForSessionApprovalWrite")) permission_decision_denied_by_content_exclusion_policy = PermissionDecisionDeniedByContentExclusionPolicy.from_dict(obj.get("PermissionDecisionDeniedByContentExclusionPolicy")) permission_decision_denied_by_permission_request_hook = PermissionDecisionDeniedByPermissionRequestHook.from_dict(obj.get("PermissionDecisionDeniedByPermissionRequestHook")) permission_decision_denied_by_rules = PermissionDecisionDeniedByRules.from_dict(obj.get("PermissionDecisionDeniedByRules")) @@ -4076,6 +4782,10 @@ def from_dict(obj: Any) -> 'RPC': permission_decision_denied_no_approval_rule_and_could_not_request_from_user = PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser.from_dict(obj.get("PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser")) permission_decision_request = PermissionDecisionRequest.from_dict(obj.get("PermissionDecisionRequest")) permission_request_result = PermissionRequestResult.from_dict(obj.get("PermissionRequestResult")) + permissions_reset_session_approvals_request = PermissionsResetSessionApprovalsRequest.from_dict(obj.get("PermissionsResetSessionApprovalsRequest")) + permissions_reset_session_approvals_result = PermissionsResetSessionApprovalsResult.from_dict(obj.get("PermissionsResetSessionApprovalsResult")) + permissions_set_approve_all_request = PermissionsSetApproveAllRequest.from_dict(obj.get("PermissionsSetApproveAllRequest")) + permissions_set_approve_all_result = PermissionsSetApproveAllResult.from_dict(obj.get("PermissionsSetApproveAllResult")) ping_request = PingRequest.from_dict(obj.get("PingRequest")) ping_result = PingResult.from_dict(obj.get("PingResult")) plan_read_result = PlanReadResult.from_dict(obj.get("PlanReadResult")) @@ -4084,6 +4794,7 @@ def from_dict(obj: Any) -> 'RPC': plugin_list = PluginList.from_dict(obj.get("PluginList")) server_skill = ServerSkill.from_dict(obj.get("ServerSkill")) server_skill_list = ServerSkillList.from_dict(obj.get("ServerSkillList")) + session_auth_status = SessionAuthStatus.from_dict(obj.get("SessionAuthStatus")) session_fs_append_file_request = SessionFSAppendFileRequest.from_dict(obj.get("SessionFsAppendFileRequest")) session_fs_error = SessionFSError.from_dict(obj.get("SessionFsError")) session_fs_error_code = SessionFSErrorCode(obj.get("SessionFsErrorCode")) @@ -4159,10 +4870,11 @@ def from_dict(obj: Any) -> 'RPC': workspaces_list_files_result = WorkspacesListFilesResult.from_dict(obj.get("WorkspacesListFilesResult")) workspaces_read_file_request = WorkspacesReadFileRequest.from_dict(obj.get("WorkspacesReadFileRequest")) workspaces_read_file_result = WorkspacesReadFileResult.from_dict(obj.get("WorkspacesReadFileResult")) - return RPC(account_get_quota_result, account_quota_snapshot, agent_get_current_result, agent_info, agent_list, agent_reload_result, agent_select_request, agent_select_result, commands_handle_pending_command_request, commands_handle_pending_command_result, current_model, discovered_mcp_server, discovered_mcp_server_source, discovered_mcp_server_type, extension, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, filter_mapping, filter_mapping_string, filter_mapping_value, fleet_start_request, fleet_start_result, handle_tool_call_result, history_compact_context_window, history_compact_result, history_truncate_request, history_truncate_result, instructions_get_sources_result, instructions_sources, instructions_sources_location, instructions_sources_type, log_request, log_result, mcp_config_add_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_server, mcp_server_config, mcp_server_config_http, mcp_server_config_http_type, mcp_server_config_local, mcp_server_config_local_type, mcp_server_list, mcp_server_source, mcp_server_status, model, model_billing, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_policy, model_switch_to_request, model_switch_to_result, mode_set_request, name_get_result, name_set_request, permission_decision, permission_decision_approved, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_request, permission_request_result, ping_request, ping_result, plan_read_result, plan_update_request, plugin, plugin_list, server_skill, server_skill_list, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_log_level, session_mode, sessions_fork_request, sessions_fork_result, shell_exec_request, shell_exec_result, shell_kill_request, shell_kill_result, shell_kill_signal, skill, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, tool, tool_call_result, tool_list, tools_handle_pending_tool_call, tools_handle_pending_tool_call_request, tools_list_request, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_handle_pending_elicitation_request, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_usage, workspaces_create_file_request, workspaces_get_workspace_result, workspaces_list_files_result, workspaces_read_file_request, workspaces_read_file_result) + return RPC(account_get_quota_request, account_get_quota_result, account_quota_snapshot, agent_get_current_result, agent_info, agent_list, agent_reload_result, agent_select_request, agent_select_result, auth_info_type, commands_handle_pending_command_request, commands_handle_pending_command_result, current_model, discovered_mcp_server, discovered_mcp_server_source, discovered_mcp_server_type, extension, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, filter_mapping, filter_mapping_string, filter_mapping_value, fleet_start_request, fleet_start_result, handle_tool_call_result, history_compact_context_window, history_compact_result, history_truncate_request, history_truncate_result, instructions_get_sources_result, instructions_sources, instructions_sources_location, instructions_sources_type, log_request, log_result, mcp_config_add_request, mcp_config_disable_request, mcp_config_enable_request, mcp_config_list, mcp_config_remove_request, mcp_config_update_request, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_oauth_login_request, mcp_oauth_login_result, mcp_server, mcp_server_config, mcp_server_config_http, mcp_server_config_http_type, mcp_server_config_local, mcp_server_config_local_type, mcp_server_list, mcp_server_source, mcp_server_status, model, model_billing, model_capabilities, model_capabilities_limits, model_capabilities_limits_vision, model_capabilities_override, model_capabilities_override_limits, model_capabilities_override_limits_vision, model_capabilities_override_supports, model_capabilities_supports, model_list, model_policy, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, name_get_result, name_set_request, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_location_approval, permission_decision_approved_for_location_approval_commands, permission_decision_approved_for_location_approval_custom_tool, permission_decision_approved_for_location_approval_mcp, permission_decision_approved_for_location_approval_mcp_sampling, permission_decision_approved_for_location_approval_memory, permission_decision_approved_for_location_approval_write, permission_decision_approved_for_session, permission_decision_approved_for_session_approval, permission_decision_approved_for_session_approval_commands, permission_decision_approved_for_session_approval_custom_tool, permission_decision_approved_for_session_approval_mcp, permission_decision_approved_for_session_approval_mcp_sampling, permission_decision_approved_for_session_approval_memory, permission_decision_approved_for_session_approval_write, permission_decision_denied_by_content_exclusion_policy, permission_decision_denied_by_permission_request_hook, permission_decision_denied_by_rules, permission_decision_denied_interactively_by_user, permission_decision_denied_no_approval_rule_and_could_not_request_from_user, permission_decision_request, permission_request_result, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_approve_all_request, permissions_set_approve_all_result, ping_request, ping_result, plan_read_result, plan_update_request, plugin, plugin_list, server_skill, server_skill_list, session_auth_status, session_fs_append_file_request, session_fs_error, session_fs_error_code, session_fs_exists_request, session_fs_exists_result, session_fs_mkdir_request, session_fs_readdir_request, session_fs_readdir_result, session_fs_readdir_with_types_entry, session_fs_readdir_with_types_entry_type, session_fs_readdir_with_types_request, session_fs_readdir_with_types_result, session_fs_read_file_request, session_fs_read_file_result, session_fs_rename_request, session_fs_rm_request, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_log_level, session_mode, sessions_fork_request, sessions_fork_result, shell_exec_request, shell_exec_result, shell_kill_request, shell_kill_result, shell_kill_signal, skill, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, tool, tool_call_result, tool_list, tools_handle_pending_tool_call, tools_handle_pending_tool_call_request, tools_list_request, ui_elicitation_array_any_of_field, ui_elicitation_array_any_of_field_items, ui_elicitation_array_any_of_field_items_any_of, ui_elicitation_array_enum_field, ui_elicitation_array_enum_field_items, ui_elicitation_field_value, ui_elicitation_request, ui_elicitation_response, ui_elicitation_response_action, ui_elicitation_response_content, ui_elicitation_result, ui_elicitation_schema, ui_elicitation_schema_property, ui_elicitation_schema_property_boolean, ui_elicitation_schema_property_number, ui_elicitation_schema_property_number_type, ui_elicitation_schema_property_string, ui_elicitation_schema_property_string_format, ui_elicitation_string_enum_field, ui_elicitation_string_one_of_field, ui_elicitation_string_one_of_field_one_of, ui_handle_pending_elicitation_request, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_usage, workspaces_create_file_request, workspaces_get_workspace_result, workspaces_list_files_result, workspaces_read_file_request, workspaces_read_file_result) def to_dict(self) -> dict: result: dict = {} + result["AccountGetQuotaRequest"] = to_class(AccountGetQuotaRequest, self.account_get_quota_request) result["AccountGetQuotaResult"] = to_class(AccountGetQuotaResult, self.account_get_quota_result) result["AccountQuotaSnapshot"] = to_class(AccountQuotaSnapshot, self.account_quota_snapshot) result["AgentGetCurrentResult"] = to_class(AgentGetCurrentResult, self.agent_get_current_result) @@ -4171,6 +4883,7 @@ def to_dict(self) -> dict: result["AgentReloadResult"] = to_class(AgentReloadResult, self.agent_reload_result) result["AgentSelectRequest"] = to_class(AgentSelectRequest, self.agent_select_request) result["AgentSelectResult"] = to_class(AgentSelectResult, self.agent_select_result) + result["AuthInfoType"] = to_enum(AuthInfoType, self.auth_info_type) result["CommandsHandlePendingCommandRequest"] = to_class(CommandsHandlePendingCommandRequest, self.commands_handle_pending_command_request) result["CommandsHandlePendingCommandResult"] = to_class(CommandsHandlePendingCommandResult, self.commands_handle_pending_command_result) result["CurrentModel"] = to_class(CurrentModel, self.current_model) @@ -4200,6 +4913,8 @@ def to_dict(self) -> dict: result["LogRequest"] = to_class(LogRequest, self.log_request) result["LogResult"] = to_class(LogResult, self.log_result) result["McpConfigAddRequest"] = to_class(MCPConfigAddRequest, self.mcp_config_add_request) + result["McpConfigDisableRequest"] = to_class(MCPConfigDisableRequest, self.mcp_config_disable_request) + result["McpConfigEnableRequest"] = to_class(MCPConfigEnableRequest, self.mcp_config_enable_request) result["McpConfigList"] = to_class(MCPConfigList, self.mcp_config_list) result["McpConfigRemoveRequest"] = to_class(MCPConfigRemoveRequest, self.mcp_config_remove_request) result["McpConfigUpdateRequest"] = to_class(MCPConfigUpdateRequest, self.mcp_config_update_request) @@ -4207,6 +4922,8 @@ def to_dict(self) -> dict: result["McpDiscoverRequest"] = to_class(MCPDiscoverRequest, self.mcp_discover_request) result["McpDiscoverResult"] = to_class(MCPDiscoverResult, self.mcp_discover_result) result["McpEnableRequest"] = to_class(MCPEnableRequest, self.mcp_enable_request) + result["McpOauthLoginRequest"] = to_class(MCPOauthLoginRequest, self.mcp_oauth_login_request) + result["McpOauthLoginResult"] = to_class(MCPOauthLoginResult, self.mcp_oauth_login_result) result["McpServer"] = to_class(MCPServer, self.mcp_server) result["McpServerConfig"] = to_class(MCPServerConfig, self.mcp_server_config) result["McpServerConfigHttp"] = to_class(MCPServerConfigHTTP, self.mcp_server_config_http) @@ -4228,6 +4945,7 @@ def to_dict(self) -> dict: result["ModelCapabilitiesSupports"] = to_class(ModelCapabilitiesSupports, self.model_capabilities_supports) result["ModelList"] = to_class(ModelList, self.model_list) result["ModelPolicy"] = to_class(ModelPolicy, self.model_policy) + result["ModelsListRequest"] = to_class(ModelsListRequest, self.models_list_request) result["ModelSwitchToRequest"] = to_class(ModelSwitchToRequest, self.model_switch_to_request) result["ModelSwitchToResult"] = to_class(ModelSwitchToResult, self.model_switch_to_result) result["ModeSetRequest"] = to_class(ModeSetRequest, self.mode_set_request) @@ -4235,6 +4953,22 @@ def to_dict(self) -> dict: result["NameSetRequest"] = to_class(NameSetRequest, self.name_set_request) result["PermissionDecision"] = to_class(PermissionDecision, self.permission_decision) result["PermissionDecisionApproved"] = to_class(PermissionDecisionApproved, self.permission_decision_approved) + result["PermissionDecisionApprovedForLocation"] = to_class(PermissionDecisionApprovedForLocation, self.permission_decision_approved_for_location) + result["PermissionDecisionApprovedForLocationApproval"] = to_class(PermissionDecisionApprovedForLocationApproval, self.permission_decision_approved_for_location_approval) + result["PermissionDecisionApprovedForLocationApprovalCommands"] = to_class(PermissionDecisionApprovedForLocationApprovalCommands, self.permission_decision_approved_for_location_approval_commands) + result["PermissionDecisionApprovedForLocationApprovalCustomTool"] = to_class(PermissionDecisionApprovedForLocationApprovalCustomTool, self.permission_decision_approved_for_location_approval_custom_tool) + result["PermissionDecisionApprovedForLocationApprovalMcp"] = to_class(PermissionDecisionApprovedForLocationApprovalMCP, self.permission_decision_approved_for_location_approval_mcp) + result["PermissionDecisionApprovedForLocationApprovalMcpSampling"] = to_class(PermissionDecisionApprovedForLocationApprovalMCPSampling, self.permission_decision_approved_for_location_approval_mcp_sampling) + result["PermissionDecisionApprovedForLocationApprovalMemory"] = to_class(PermissionDecisionApprovedForLocationApprovalMemory, self.permission_decision_approved_for_location_approval_memory) + result["PermissionDecisionApprovedForLocationApprovalWrite"] = to_class(PermissionDecisionApprovedForLocationApprovalWrite, self.permission_decision_approved_for_location_approval_write) + result["PermissionDecisionApprovedForSession"] = to_class(PermissionDecisionApprovedForSession, self.permission_decision_approved_for_session) + result["PermissionDecisionApprovedForSessionApproval"] = to_class(PermissionDecisionApprovedForSessionApproval, self.permission_decision_approved_for_session_approval) + result["PermissionDecisionApprovedForSessionApprovalCommands"] = to_class(PermissionDecisionApprovedForSessionApprovalCommands, self.permission_decision_approved_for_session_approval_commands) + result["PermissionDecisionApprovedForSessionApprovalCustomTool"] = to_class(PermissionDecisionApprovedForSessionApprovalCustomTool, self.permission_decision_approved_for_session_approval_custom_tool) + result["PermissionDecisionApprovedForSessionApprovalMcp"] = to_class(PermissionDecisionApprovedForSessionApprovalMCP, self.permission_decision_approved_for_session_approval_mcp) + result["PermissionDecisionApprovedForSessionApprovalMcpSampling"] = to_class(PermissionDecisionApprovedForSessionApprovalMCPSampling, self.permission_decision_approved_for_session_approval_mcp_sampling) + result["PermissionDecisionApprovedForSessionApprovalMemory"] = to_class(PermissionDecisionApprovedForSessionApprovalMemory, self.permission_decision_approved_for_session_approval_memory) + result["PermissionDecisionApprovedForSessionApprovalWrite"] = to_class(PermissionDecisionApprovedForSessionApprovalWrite, self.permission_decision_approved_for_session_approval_write) result["PermissionDecisionDeniedByContentExclusionPolicy"] = to_class(PermissionDecisionDeniedByContentExclusionPolicy, self.permission_decision_denied_by_content_exclusion_policy) result["PermissionDecisionDeniedByPermissionRequestHook"] = to_class(PermissionDecisionDeniedByPermissionRequestHook, self.permission_decision_denied_by_permission_request_hook) result["PermissionDecisionDeniedByRules"] = to_class(PermissionDecisionDeniedByRules, self.permission_decision_denied_by_rules) @@ -4242,6 +4976,10 @@ def to_dict(self) -> dict: result["PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser"] = to_class(PermissionDecisionDeniedNoApprovalRuleAndCouldNotRequestFromUser, self.permission_decision_denied_no_approval_rule_and_could_not_request_from_user) result["PermissionDecisionRequest"] = to_class(PermissionDecisionRequest, self.permission_decision_request) result["PermissionRequestResult"] = to_class(PermissionRequestResult, self.permission_request_result) + result["PermissionsResetSessionApprovalsRequest"] = to_class(PermissionsResetSessionApprovalsRequest, self.permissions_reset_session_approvals_request) + result["PermissionsResetSessionApprovalsResult"] = to_class(PermissionsResetSessionApprovalsResult, self.permissions_reset_session_approvals_result) + result["PermissionsSetApproveAllRequest"] = to_class(PermissionsSetApproveAllRequest, self.permissions_set_approve_all_request) + result["PermissionsSetApproveAllResult"] = to_class(PermissionsSetApproveAllResult, self.permissions_set_approve_all_result) result["PingRequest"] = to_class(PingRequest, self.ping_request) result["PingResult"] = to_class(PingResult, self.ping_result) result["PlanReadResult"] = to_class(PlanReadResult, self.plan_read_result) @@ -4250,6 +4988,7 @@ def to_dict(self) -> dict: result["PluginList"] = to_class(PluginList, self.plugin_list) result["ServerSkill"] = to_class(ServerSkill, self.server_skill) result["ServerSkillList"] = to_class(ServerSkillList, self.server_skill_list) + result["SessionAuthStatus"] = to_class(SessionAuthStatus, self.session_auth_status) result["SessionFsAppendFileRequest"] = to_class(SessionFSAppendFileRequest, self.session_fs_append_file_request) result["SessionFsError"] = to_class(SessionFSError, self.session_fs_error) result["SessionFsErrorCode"] = to_enum(SessionFSErrorCode, self.session_fs_error_code) @@ -4366,8 +5105,9 @@ class ServerModelsApi: def __init__(self, client: "JsonRpcClient"): self._client = client - async def list(self, *, timeout: float | None = None) -> ModelList: - return ModelList.from_dict(_patch_model_capabilities(await self._client.request("models.list", {}, **_timeout_kwargs(timeout)))) + async def list(self, params: ModelsListRequest | None = None, *, timeout: float | None = None) -> ModelList: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} if params is not None else {} + return ModelList.from_dict(_patch_model_capabilities(await self._client.request("models.list", params_dict, **_timeout_kwargs(timeout)))) class ServerToolsApi: @@ -4383,8 +5123,9 @@ class ServerAccountApi: def __init__(self, client: "JsonRpcClient"): self._client = client - async def get_quota(self, *, timeout: float | None = None) -> AccountGetQuotaResult: - return AccountGetQuotaResult.from_dict(await self._client.request("account.getQuota", {}, **_timeout_kwargs(timeout))) + async def get_quota(self, params: AccountGetQuotaRequest | None = None, *, timeout: float | None = None) -> AccountGetQuotaResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} if params is not None else {} + return AccountGetQuotaResult.from_dict(await self._client.request("account.getQuota", params_dict, **_timeout_kwargs(timeout))) class ServerMcpConfigApi: @@ -4406,6 +5147,14 @@ async def remove(self, params: MCPConfigRemoveRequest, *, timeout: float | None params_dict = {k: v for k, v in params.to_dict().items() if v is not None} await self._client.request("mcp.config.remove", params_dict, **_timeout_kwargs(timeout)) + async def enable(self, params: MCPConfigEnableRequest, *, timeout: float | None = None) -> None: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + await self._client.request("mcp.config.enable", params_dict, **_timeout_kwargs(timeout)) + + async def disable(self, params: MCPConfigDisableRequest, *, timeout: float | None = None) -> None: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + await self._client.request("mcp.config.disable", params_dict, **_timeout_kwargs(timeout)) + class ServerMcpApi: def __init__(self, client: "JsonRpcClient"): @@ -4472,6 +5221,15 @@ async def ping(self, params: PingRequest, *, timeout: float | None = None) -> Pi return PingResult.from_dict(await self._client.request("ping", params_dict, **_timeout_kwargs(timeout))) +class AuthApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def get_status(self, *, timeout: float | None = None) -> SessionAuthStatus: + return SessionAuthStatus.from_dict(await self._client.request("session.auth.getStatus", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + + class ModelApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client @@ -4481,7 +5239,7 @@ async def get_current(self, *, timeout: float | None = None) -> CurrentModel: return CurrentModel.from_dict(await self._client.request("session.model.getCurrent", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def switch_to(self, params: ModelSwitchToRequest, *, timeout: float | None = None) -> ModelSwitchToResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return ModelSwitchToResult.from_dict(await self._client.request("session.model.switchTo", params_dict, **_timeout_kwargs(timeout))) @@ -4495,7 +5253,7 @@ async def get(self, *, timeout: float | None = None) -> SessionMode: return SessionMode(await self._client.request("session.mode.get", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def set(self, params: ModeSetRequest, *, timeout: float | None = None) -> None: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id await self._client.request("session.mode.set", params_dict, **_timeout_kwargs(timeout)) @@ -4509,7 +5267,7 @@ async def get(self, *, timeout: float | None = None) -> NameGetResult: return NameGetResult.from_dict(await self._client.request("session.name.get", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def set(self, params: NameSetRequest, *, timeout: float | None = None) -> None: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id await self._client.request("session.name.set", params_dict, **_timeout_kwargs(timeout)) @@ -4523,7 +5281,7 @@ async def read(self, *, timeout: float | None = None) -> PlanReadResult: return PlanReadResult.from_dict(await self._client.request("session.plan.read", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def update(self, params: PlanUpdateRequest, *, timeout: float | None = None) -> None: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id await self._client.request("session.plan.update", params_dict, **_timeout_kwargs(timeout)) @@ -4543,12 +5301,12 @@ async def list_files(self, *, timeout: float | None = None) -> WorkspacesListFil return WorkspacesListFilesResult.from_dict(await self._client.request("session.workspaces.listFiles", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def read_file(self, params: WorkspacesReadFileRequest, *, timeout: float | None = None) -> WorkspacesReadFileResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return WorkspacesReadFileResult.from_dict(await self._client.request("session.workspaces.readFile", params_dict, **_timeout_kwargs(timeout))) async def create_file(self, params: WorkspacesCreateFileRequest, *, timeout: float | None = None) -> None: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id await self._client.request("session.workspaces.createFile", params_dict, **_timeout_kwargs(timeout)) @@ -4569,7 +5327,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._session_id = session_id async def start(self, params: FleetStartRequest, *, timeout: float | None = None) -> FleetStartResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return FleetStartResult.from_dict(await self._client.request("session.fleet.start", params_dict, **_timeout_kwargs(timeout))) @@ -4587,7 +5345,7 @@ async def get_current(self, *, timeout: float | None = None) -> AgentGetCurrentR return AgentGetCurrentResult.from_dict(await self._client.request("session.agent.getCurrent", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def select(self, params: AgentSelectRequest, *, timeout: float | None = None) -> AgentSelectResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return AgentSelectResult.from_dict(await self._client.request("session.agent.select", params_dict, **_timeout_kwargs(timeout))) @@ -4608,12 +5366,12 @@ async def list(self, *, timeout: float | None = None) -> SkillList: return SkillList.from_dict(await self._client.request("session.skills.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def enable(self, params: SkillsEnableRequest, *, timeout: float | None = None) -> None: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id await self._client.request("session.skills.enable", params_dict, **_timeout_kwargs(timeout)) async def disable(self, params: SkillsDisableRequest, *, timeout: float | None = None) -> None: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id await self._client.request("session.skills.disable", params_dict, **_timeout_kwargs(timeout)) @@ -4621,22 +5379,35 @@ async def reload(self, *, timeout: float | None = None) -> None: await self._client.request("session.skills.reload", {"sessionId": self._session_id}, **_timeout_kwargs(timeout)) +# Experimental: this API group is experimental and may change or be removed. +class McpOauthApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def login(self, params: MCPOauthLoginRequest, *, timeout: float | None = None) -> MCPOauthLoginResult: + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return MCPOauthLoginResult.from_dict(await self._client.request("session.mcp.oauth.login", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class McpApi: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id + self.oauth = McpOauthApi(client, session_id) async def list(self, *, timeout: float | None = None) -> MCPServerList: return MCPServerList.from_dict(await self._client.request("session.mcp.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def enable(self, params: MCPEnableRequest, *, timeout: float | None = None) -> None: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id await self._client.request("session.mcp.enable", params_dict, **_timeout_kwargs(timeout)) async def disable(self, params: MCPDisableRequest, *, timeout: float | None = None) -> None: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id await self._client.request("session.mcp.disable", params_dict, **_timeout_kwargs(timeout)) @@ -4664,12 +5435,12 @@ async def list(self, *, timeout: float | None = None) -> ExtensionList: return ExtensionList.from_dict(await self._client.request("session.extensions.list", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def enable(self, params: ExtensionsEnableRequest, *, timeout: float | None = None) -> None: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id await self._client.request("session.extensions.enable", params_dict, **_timeout_kwargs(timeout)) async def disable(self, params: ExtensionsDisableRequest, *, timeout: float | None = None) -> None: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id await self._client.request("session.extensions.disable", params_dict, **_timeout_kwargs(timeout)) @@ -4683,7 +5454,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._session_id = session_id async def handle_pending_tool_call(self, params: ToolsHandlePendingToolCallRequest, *, timeout: float | None = None) -> HandleToolCallResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return HandleToolCallResult.from_dict(await self._client.request("session.tools.handlePendingToolCall", params_dict, **_timeout_kwargs(timeout))) @@ -4694,7 +5465,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._session_id = session_id async def handle_pending_command(self, params: CommandsHandlePendingCommandRequest, *, timeout: float | None = None) -> CommandsHandlePendingCommandResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return CommandsHandlePendingCommandResult.from_dict(await self._client.request("session.commands.handlePendingCommand", params_dict, **_timeout_kwargs(timeout))) @@ -4705,12 +5476,12 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._session_id = session_id async def elicitation(self, params: UIElicitationRequest, *, timeout: float | None = None) -> UIElicitationResponse: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return UIElicitationResponse.from_dict(await self._client.request("session.ui.elicitation", params_dict, **_timeout_kwargs(timeout))) async def handle_pending_elicitation(self, params: UIHandlePendingElicitationRequest, *, timeout: float | None = None) -> UIElicitationResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return UIElicitationResult.from_dict(await self._client.request("session.ui.handlePendingElicitation", params_dict, **_timeout_kwargs(timeout))) @@ -4721,10 +5492,18 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._session_id = session_id async def handle_pending_permission_request(self, params: PermissionDecisionRequest, *, timeout: float | None = None) -> PermissionRequestResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return PermissionRequestResult.from_dict(await self._client.request("session.permissions.handlePendingPermissionRequest", params_dict, **_timeout_kwargs(timeout))) + async def set_approve_all(self, params: PermissionsSetApproveAllRequest, *, timeout: float | None = None) -> PermissionsSetApproveAllResult: + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return PermissionsSetApproveAllResult.from_dict(await self._client.request("session.permissions.setApproveAll", params_dict, **_timeout_kwargs(timeout))) + + async def reset_session_approvals(self, *, timeout: float | None = None) -> PermissionsResetSessionApprovalsResult: + return PermissionsResetSessionApprovalsResult.from_dict(await self._client.request("session.permissions.resetSessionApprovals", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) + class ShellApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -4732,12 +5511,12 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._session_id = session_id async def exec(self, params: ShellExecRequest, *, timeout: float | None = None) -> ShellExecResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return ShellExecResult.from_dict(await self._client.request("session.shell.exec", params_dict, **_timeout_kwargs(timeout))) async def kill(self, params: ShellKillRequest, *, timeout: float | None = None) -> ShellKillResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return ShellKillResult.from_dict(await self._client.request("session.shell.kill", params_dict, **_timeout_kwargs(timeout))) @@ -4752,7 +5531,7 @@ async def compact(self, *, timeout: float | None = None) -> HistoryCompactResult return HistoryCompactResult.from_dict(await self._client.request("session.history.compact", {"sessionId": self._session_id}, **_timeout_kwargs(timeout))) async def truncate(self, params: HistoryTruncateRequest, *, timeout: float | None = None) -> HistoryTruncateResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return HistoryTruncateResult.from_dict(await self._client.request("session.history.truncate", params_dict, **_timeout_kwargs(timeout))) @@ -4772,6 +5551,7 @@ class SessionRpc: def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id + self.auth = AuthApi(client, session_id) self.model = ModelApi(client, session_id) self.mode = ModeApi(client, session_id) self.name = NameApi(client, session_id) @@ -4793,7 +5573,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.usage = UsageApi(client, session_id) async def log(self, params: LogRequest, *, timeout: float | None = None) -> LogResult: - params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} params_dict["sessionId"] = self._session_id return LogResult.from_dict(await self._client.request("session.log", params_dict, **_timeout_kwargs(timeout))) diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 1b3452bd4..b7a3ef432 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -167,6 +167,8 @@ class SessionEventType(Enum): COMMAND_QUEUED = "command.queued" COMMAND_EXECUTE = "command.execute" COMMAND_COMPLETED = "command.completed" + AUTO_MODE_SWITCH_REQUESTED = "auto_mode_switch.requested" + AUTO_MODE_SWITCH_COMPLETED = "auto_mode_switch.completed" COMMANDS_CHANGED = "commands.changed" CAPABILITIES_CHANGED = "capabilities.changed" EXIT_PLAN_MODE_REQUESTED = "exit_plan_mode.requested" @@ -744,6 +746,53 @@ def to_dict(self) -> dict: return result +@dataclass +class AutoModeSwitchCompletedData: + "Auto mode switch completion notification" + request_id: str + response: str + + @staticmethod + def from_dict(obj: Any) -> "AutoModeSwitchCompletedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + response = from_str(obj.get("response")) + return AutoModeSwitchCompletedData( + request_id=request_id, + response=response, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["response"] = from_str(self.response) + return result + + +@dataclass +class AutoModeSwitchRequestedData: + "Auto mode switch request notification requiring user approval" + request_id: str + error_code: str | None = None + + @staticmethod + def from_dict(obj: Any) -> "AutoModeSwitchRequestedData": + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + error_code = from_union([from_none, from_str], obj.get("errorCode")) + return AutoModeSwitchRequestedData( + request_id=request_id, + error_code=error_code, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + if self.error_code is not None: + result["errorCode"] = from_union([from_none, from_str], self.error_code) + return result + + @dataclass class CapabilitiesChangedData: "Session capability change notification" @@ -901,28 +950,105 @@ def to_dict(self) -> dict: @dataclass class CompactionCompleteCompactionTokensUsed: - "Token usage breakdown for the compaction LLM call" - cached_input: float - input: float - output: float + "Token usage breakdown for the compaction LLM call (aligned with assistant.usage format)" + cache_read_tokens: float | None = None + cache_write_tokens: float | None = None + copilot_usage: CompactionCompleteCompactionTokensUsedCopilotUsage | None = None + duration: float | None = None + input_tokens: float | None = None + model: str | None = None + output_tokens: float | None = None @staticmethod def from_dict(obj: Any) -> "CompactionCompleteCompactionTokensUsed": assert isinstance(obj, dict) - cached_input = from_float(obj.get("cachedInput")) - input = from_float(obj.get("input")) - output = from_float(obj.get("output")) + cache_read_tokens = from_union([from_none, from_float], obj.get("cacheReadTokens")) + cache_write_tokens = from_union([from_none, from_float], obj.get("cacheWriteTokens")) + copilot_usage = from_union([from_none, CompactionCompleteCompactionTokensUsedCopilotUsage.from_dict], obj.get("copilotUsage")) + duration = from_union([from_none, from_float], obj.get("duration")) + input_tokens = from_union([from_none, from_float], obj.get("inputTokens")) + model = from_union([from_none, from_str], obj.get("model")) + output_tokens = from_union([from_none, from_float], obj.get("outputTokens")) return CompactionCompleteCompactionTokensUsed( - cached_input=cached_input, - input=input, - output=output, + cache_read_tokens=cache_read_tokens, + cache_write_tokens=cache_write_tokens, + copilot_usage=copilot_usage, + duration=duration, + input_tokens=input_tokens, + model=model, + output_tokens=output_tokens, ) def to_dict(self) -> dict: result: dict = {} - result["cachedInput"] = to_float(self.cached_input) - result["input"] = to_float(self.input) - result["output"] = to_float(self.output) + if self.cache_read_tokens is not None: + result["cacheReadTokens"] = from_union([from_none, to_float], self.cache_read_tokens) + if self.cache_write_tokens is not None: + result["cacheWriteTokens"] = from_union([from_none, to_float], self.cache_write_tokens) + if self.copilot_usage is not None: + result["copilotUsage"] = from_union([from_none, lambda x: to_class(CompactionCompleteCompactionTokensUsedCopilotUsage, x)], self.copilot_usage) + if self.duration is not None: + result["duration"] = from_union([from_none, to_float], self.duration) + if self.input_tokens is not None: + result["inputTokens"] = from_union([from_none, to_float], self.input_tokens) + if self.model is not None: + result["model"] = from_union([from_none, from_str], self.model) + if self.output_tokens is not None: + result["outputTokens"] = from_union([from_none, to_float], self.output_tokens) + return result + + +@dataclass +class CompactionCompleteCompactionTokensUsedCopilotUsage: + "Per-request cost and usage data from the CAPI copilot_usage response field" + token_details: list[CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail] + total_nano_aiu: float + + @staticmethod + def from_dict(obj: Any) -> "CompactionCompleteCompactionTokensUsedCopilotUsage": + assert isinstance(obj, dict) + token_details = from_list(CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail.from_dict, obj.get("tokenDetails")) + total_nano_aiu = from_float(obj.get("totalNanoAiu")) + return CompactionCompleteCompactionTokensUsedCopilotUsage( + token_details=token_details, + total_nano_aiu=total_nano_aiu, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["tokenDetails"] = from_list(lambda x: to_class(CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail, x), self.token_details) + result["totalNanoAiu"] = to_float(self.total_nano_aiu) + return result + + +@dataclass +class CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail: + "Token usage detail for a single billing category" + batch_size: float + cost_per_batch: float + token_count: float + token_type: str + + @staticmethod + def from_dict(obj: Any) -> "CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail": + assert isinstance(obj, dict) + batch_size = from_float(obj.get("batchSize")) + cost_per_batch = from_float(obj.get("costPerBatch")) + token_count = from_float(obj.get("tokenCount")) + token_type = from_str(obj.get("tokenType")) + return CompactionCompleteCompactionTokensUsedCopilotUsageTokenDetail( + batch_size=batch_size, + cost_per_batch=cost_per_batch, + token_count=token_count, + token_type=token_type, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["batchSize"] = to_float(self.batch_size) + result["costPerBatch"] = to_float(self.cost_per_batch) + result["tokenCount"] = to_float(self.token_count) + result["tokenType"] = from_str(self.token_type) return result @@ -3997,6 +4123,8 @@ class McpServersLoadedServerStatus(Enum): class PermissionCompletedKind(Enum): "The outcome of the permission request" APPROVED = "approved" + APPROVED_FOR_SESSION = "approved-for-session" + APPROVED_FOR_LOCATION = "approved-for-location" DENIED_BY_RULES = "denied-by-rules" DENIED_NO_APPROVAL_RULE_AND_COULD_NOT_REQUEST_FROM_USER = "denied-no-approval-rule-and-could-not-request-from-user" DENIED_INTERACTIVELY_BY_USER = "denied-interactively-by-user" @@ -4114,7 +4242,7 @@ class WorkspaceFileChangedOperation(Enum): UPDATE = "update" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionPlanChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantUsageData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionExtensionsLoadedData | RawSessionEventData | Data +SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionPlanChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantUsageData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionExtensionsLoadedData | RawSessionEventData | Data @dataclass @@ -4201,6 +4329,8 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.COMMAND_QUEUED: data = CommandQueuedData.from_dict(data_obj) case SessionEventType.COMMAND_EXECUTE: data = CommandExecuteData.from_dict(data_obj) case SessionEventType.COMMAND_COMPLETED: data = CommandCompletedData.from_dict(data_obj) + case SessionEventType.AUTO_MODE_SWITCH_REQUESTED: data = AutoModeSwitchRequestedData.from_dict(data_obj) + case SessionEventType.AUTO_MODE_SWITCH_COMPLETED: data = AutoModeSwitchCompletedData.from_dict(data_obj) case SessionEventType.COMMANDS_CHANGED: data = CommandsChangedData.from_dict(data_obj) case SessionEventType.CAPABILITIES_CHANGED: data = CapabilitiesChangedData.from_dict(data_obj) case SessionEventType.EXIT_PLAN_MODE_REQUESTED: data = ExitPlanModeRequestedData.from_dict(data_obj) diff --git a/python/e2e/test_per_session_auth.py b/python/e2e/test_per_session_auth.py new file mode 100644 index 000000000..670236f59 --- /dev/null +++ b/python/e2e/test_per_session_auth.py @@ -0,0 +1,115 @@ +"""E2E Per-session GitHub auth tests""" + +import pytest + +from copilot.session import PermissionHandler + +from .testharness import E2ETestContext + +pytestmark = pytest.mark.asyncio(loop_scope="module") + + +@pytest.fixture(scope="module") +async def auth_ctx(ctx: E2ETestContext): + """Configure per-token user responses on the proxy before tests run.""" + proxy_url = ctx.proxy_url + + # Redirect GitHub API calls to the proxy so per-session auth token + # resolution (fetchCopilotUser) is intercepted. Must be set before the + # CLI subprocess is spawned (i.e., before the first create_session call). + ctx.client._config.env["COPILOT_DEBUG_GITHUB_API_URL"] = proxy_url + + await ctx.set_copilot_user_by_token( + "token-alice", + { + "login": "alice", + "copilot_plan": "individual_pro", + "endpoints": { + "api": proxy_url, + "telemetry": "https://localhost:1/telemetry", + }, + "analytics_tracking_id": "alice-tracking-id", + }, + ) + + await ctx.set_copilot_user_by_token( + "token-bob", + { + "login": "bob", + "copilot_plan": "business", + "endpoints": { + "api": proxy_url, + "telemetry": "https://localhost:1/telemetry", + }, + "analytics_tracking_id": "bob-tracking-id", + }, + ) + + return ctx + + +class TestPerSessionAuth: + async def test_should_create_session_with_github_token_and_check_auth_status( + self, auth_ctx: E2ETestContext + ): + session = await auth_ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + github_token="token-alice", + ) + + auth_status = await session.rpc.auth.get_status() + assert auth_status.is_authenticated is True + assert auth_status.login == "alice" + assert auth_status.copilot_plan == "individual_pro" + + await session.disconnect() + + async def test_should_isolate_auth_between_sessions_with_different_tokens( + self, auth_ctx: E2ETestContext + ): + session_a = await auth_ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + github_token="token-alice", + ) + session_b = await auth_ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + github_token="token-bob", + ) + + status_a = await session_a.rpc.auth.get_status() + status_b = await session_b.rpc.auth.get_status() + + assert status_a.is_authenticated is True + assert status_a.login == "alice" + assert status_a.copilot_plan == "individual_pro" + + assert status_b.is_authenticated is True + assert status_b.login == "bob" + assert status_b.copilot_plan == "business" + + await session_a.disconnect() + await session_b.disconnect() + + async def test_should_return_unauthenticated_when_no_token_provided( + self, auth_ctx: E2ETestContext + ): + session = await auth_ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + ) + + auth_status = await session.rpc.auth.get_status() + # Without a per-session token, there is no per-session identity. + # In CI the process-level fake token may still authenticate globally, + # so we check login rather than is_authenticated. + assert auth_status.login is None + + await session.disconnect() + + async def test_should_error_when_creating_session_with_invalid_token( + self, auth_ctx: E2ETestContext + ): + with pytest.raises(Exception): + await auth_ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + github_token="invalid-token-12345", + ) diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index 6a4bac6d2..c2028c14a 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -9,6 +9,7 @@ import shutil import tempfile from pathlib import Path +from typing import Any from copilot import CopilotClient from copilot.client import SubprocessConfig @@ -145,6 +146,12 @@ def client(self) -> CopilotClient: raise RuntimeError("Context not set up. Call setup() first.") return self._client + async def set_copilot_user_by_token(self, token: str, response: dict[str, Any]) -> 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) + async def get_exchanges(self): """Retrieve the captured HTTP exchanges from the proxy.""" if not self._proxy: diff --git a/python/e2e/testharness/proxy.py b/python/e2e/testharness/proxy.py index 65dd8bda9..e125375e0 100644 --- a/python/e2e/testharness/proxy.py +++ b/python/e2e/testharness/proxy.py @@ -106,6 +106,18 @@ async def get_exchanges(self) -> list[dict[str, Any]]: resp = await client.get(f"{self._proxy_url}/exchanges") return resp.json() + async def set_copilot_user_by_token(self, token: str, response: dict[str, Any]) -> None: + """Register a per-token response for /copilot_internal/user.""" + if not self._proxy_url: + raise RuntimeError("Proxy not started") + + async with httpx.AsyncClient() as client: + resp = await client.post( + f"{self._proxy_url}/copilot-user-config", + json={"token": token, "response": response}, + ) + assert resp.status_code == 200 + @property def url(self) -> str | None: """Return the proxy URL, or None if not started.""" diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 8416d4e40..4baf8061c 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -405,6 +405,17 @@ function findDiscriminator(variants: JSONSchema7[]): { property: string; mapping return null; } +/** Callback that resolves the C# type for a property schema within a polymorphic class. */ +type PropertyTypeResolver = ( + propSchema: JSONSchema7, + parentClassName: string, + propName: string, + isRequired: boolean, + knownTypes: Map, + nestedClasses: Map, + enumOutput: string[] +) => string; + /** * Generate a polymorphic base class and derived classes for a discriminated union. */ @@ -415,8 +426,10 @@ function generatePolymorphicClasses( knownTypes: Map, nestedClasses: Map, enumOutput: string[], - description?: string + description?: string, + propertyResolver?: PropertyTypeResolver ): string { + const resolver = propertyResolver ?? resolveSessionPropertyType; const lines: string[] = []; const discriminatorInfo = findDiscriminator(variants)!; const renamedBase = applyTypeRename(baseClassName); @@ -441,7 +454,7 @@ function generatePolymorphicClasses( for (const [constValue, variant] of discriminatorInfo.mapping) { const derivedClassName = applyTypeRename(`${baseClassName}${toPascalCase(constValue)}`); - const derivedCode = generateDerivedClass(derivedClassName, renamedBase, discriminatorProperty, constValue, variant, knownTypes, nestedClasses, enumOutput); + const derivedCode = generateDerivedClass(derivedClassName, renamedBase, discriminatorProperty, constValue, variant, knownTypes, nestedClasses, enumOutput, resolver); nestedClasses.set(derivedClassName, derivedCode); } @@ -459,7 +472,8 @@ function generateDerivedClass( schema: JSONSchema7, knownTypes: Map, nestedClasses: Map, - enumOutput: string[] + enumOutput: string[], + propertyResolver: PropertyTypeResolver ): string { const lines: string[] = []; const required = new Set(schema.required || []); @@ -480,7 +494,7 @@ function generateDerivedClass( const isReq = required.has(propName); const csharpName = toPascalCase(propName); - const csharpType = resolveSessionPropertyType(propSchema as JSONSchema7, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput); + const csharpType = propertyResolver(propSchema as JSONSchema7, className, csharpName, isReq, knownTypes, nestedClasses, enumOutput); lines.push(...xmlDocPropertyComment((propSchema as JSONSchema7).description, propName, " ")); lines.push(...emitDataAnnotations(propSchema as JSONSchema7, " ")); @@ -901,7 +915,15 @@ function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassNam if (!emittedRpcClassSchemas.has(baseClassName)) { emittedRpcClassSchemas.set(baseClassName, "polymorphic"); const nestedMap = new Map(); - const polymorphicCode = generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, rpcKnownTypes, nestedMap, rpcEnumOutput, schema.description); + const rpcPropertyResolver: PropertyTypeResolver = (propSchema, parentClass, pName, isReq, _kt, nestedCls, enumOut) => { + const nestedRpcClasses: string[] = []; + const result = resolveRpcType(propSchema, isReq, parentClass, pName, nestedRpcClasses); + for (const cls of nestedRpcClasses) { + nestedCls.set(cls.match(/class (\w+)/)?.[1] ?? cls.slice(0, 40), cls); + } + return result; + }; + const polymorphicCode = generatePolymorphicClasses(baseClassName, discriminatorInfo.property, variants, rpcKnownTypes, nestedMap, rpcEnumOutput, schema.description, rpcPropertyResolver); classes.push(polymorphicCode); for (const nested of nestedMap.values()) classes.push(nested); } diff --git a/scripts/codegen/go.ts b/scripts/codegen/go.ts index bb7d85319..c1acc4980 100644 --- a/scripts/codegen/go.ts +++ b/scripts/codegen/go.ts @@ -250,6 +250,12 @@ function schemaSourceForNamedDefinition( if (schema?.$ref && resolvedSchema) { return resolvedSchema; } + // When the schema is an anyOf/oneOf wrapper (e.g., Zod optional params producing + // `anyOf: [{ not: {} }, { $ref }]`), use the resolved object schema to avoid + // generating self-referential type aliases that crash quicktype. + if ((schema?.anyOf || schema?.oneOf) && resolvedSchema?.properties) { + return resolvedSchema; + } return schema ?? resolvedSchema ?? { type: "object" }; } diff --git a/scripts/codegen/python.ts b/scripts/codegen/python.ts index 6fe931994..6a3fe3b7d 100644 --- a/scripts/codegen/python.ts +++ b/scripts/codegen/python.ts @@ -408,6 +408,12 @@ function schemaSourceForNamedDefinition( if (schema?.$ref && resolvedSchema) { return resolvedSchema; } + // When the schema is an anyOf/oneOf wrapper (e.g., Zod optional params producing + // `anyOf: [{ not: {} }, { $ref }]`), use the resolved object schema to avoid + // generating self-referential type aliases that crash quicktype. + if ((schema?.anyOf || schema?.oneOf) && resolvedSchema?.properties) { + return resolvedSchema; + } return schema ?? resolvedSchema ?? { type: "object" }; } @@ -438,6 +444,19 @@ function pythonResultTypeName(method: RpcMethod, schemaOverride?: JSONSchema7): return getRpcSchemaTypeName(schema, toPascalCase(method.rpcMethod) + "Result"); } +/** 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 + ); +} + function pythonParamsTypeName(method: RpcMethod): string { const fallback = pythonRequestFallbackName(method); if (method.rpcMethod.startsWith("session.") && method.params?.$ref) { @@ -1813,8 +1832,9 @@ def _patch_model_capabilities(data: dict) -> dict: `ModelList.from_dict(_patch_model_capabilities(await self._client.request("models.list"`, ); // Close the extra paren opened by _patch_model_capabilities( + // Match everything from _patch_model_capabilities( up to the end of the return statement finalCode = finalCode.replace( - /(_patch_model_capabilities\(await self\._client\.request\("models\.list",\s*\{[^)]*\)[^)]*\))/, + /(_patch_model_capabilities\(await self\._client\.request\("models\.list"[^)]*\)[^)]*\))/, "$1)", ); finalCode = unwrapRedundantPythonLambdas(finalCode); @@ -1943,10 +1963,13 @@ function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: const nonSessionParams = Object.keys(paramProps).filter((k) => k !== "sessionId"); const hasParams = isSession ? nonSessionParams.length > 0 : hasSchemaPayload(effectiveParams); const paramsType = resolveType(pythonParamsTypeName(method)); + const paramsOptional = isParamsOptional(method); // Build signature with typed params + optional timeout const sig = hasParams - ? ` async def ${methodName}(self, params: ${paramsType}, *, timeout: float | None = None) -> ${resultType}:` + ? paramsOptional + ? ` async def ${methodName}(self, params: ${paramsType} | None = None, *, timeout: float | None = None) -> ${resultType}:` + : ` async def ${methodName}(self, params: ${paramsType}, *, timeout: float | None = None) -> ${resultType}:` : ` async def ${methodName}(self, *, timeout: float | None = None) -> ${resultType}:`; lines.push(sig); @@ -1986,7 +2009,11 @@ function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: if (isSession) { if (hasParams) { - lines.push(` params_dict = {k: v for k, v in params.to_dict().items() if v is not None}`); + if (paramsOptional) { + lines.push(` params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None} if params is not None else {}`); + } else { + lines.push(` params_dict: dict[str, Any] = {k: v for k, v in params.to_dict().items() if v is not None}`); + } lines.push(` params_dict["sessionId"] = self._session_id`); emitRequestCall("params_dict"); } else { @@ -1994,7 +2021,11 @@ function emitMethod(lines: string[], name: string, method: RpcMethod, isSession: } } else { if (hasParams) { - lines.push(` params_dict = {k: v for k, v in params.to_dict().items() if v is not None}`); + if (paramsOptional) { + lines.push(` params_dict = {k: v for k, v in params.to_dict().items() if v is not None} if params is not None else {}`); + } else { + lines.push(` params_dict = {k: v for k, v in params.to_dict().items() if v is not None}`); + } emitRequestCall("params_dict"); } else { emitRequestCall("{}"); diff --git a/scripts/codegen/typescript.ts b/scripts/codegen/typescript.ts index 208e05941..d032c34fd 100644 --- a/scripts/codegen/typescript.ts +++ b/scripts/codegen/typescript.ts @@ -235,6 +235,12 @@ function schemaSourceForNamedDefinition( if (schema?.$ref && resolvedSchema) { return resolvedSchema; } + // When the schema is an anyOf/oneOf wrapper (e.g., Zod optional params producing + // `anyOf: [{ not: {} }, { $ref }]`), use the resolved object schema to avoid + // generating self-referential type aliases. + if ((schema?.anyOf || schema?.oneOf) && resolvedSchema?.properties) { + return resolvedSchema; + } return schema ?? resolvedSchema ?? { type: "object" }; } @@ -251,6 +257,19 @@ function getMethodParamsSchema(method: RpcMethod): JSONSchema7 | undefined { ); } +/** 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 + ); +} + function resultTypeName(method: RpcMethod): string { return getRpcSchemaTypeName( getMethodResultSchema(method), @@ -460,14 +479,18 @@ function emitGroup(node: Record, indent: string, isSession: boo if (isSession) { 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 { bodyArg = "{ sessionId }"; } } else { if (hasParams) { - sigParams.push(`params: ${paramsType}`); + const optMark = isParamsOptional(value) ? "?" : ""; + sigParams.push(`params${optMark}: ${paramsType}`); bodyArg = "params"; } else { bodyArg = "{}"; diff --git a/scripts/codegen/utils.ts b/scripts/codegen/utils.ts index 9abc9c8fb..4a4c31f3f 100644 --- a/scripts/codegen/utils.ts +++ b/scripts/codegen/utils.ts @@ -482,7 +482,14 @@ export function resolveObjectSchema( } const singleBranch = (resolved.anyOf ?? resolved.oneOf) - ?.filter((item): item is JSONSchema7 => typeof item === "object" && (item as JSONSchema7).type !== "null"); + ?.filter((item): item is JSONSchema7 => { + if (!item || 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; + }); if (singleBranch && singleBranch.length === 1) { return resolveObjectSchema(singleBranch[0], definitions); } diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index a63c5b123..967c18084 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -56,6 +56,14 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { { toolName: "*", normalizer: normalizeLargeOutputFilepaths }, ]; + /** + * 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 `, the matching response is returned. + * If no match is found, a 401 Unauthorized response is returned. + */ + private copilotUserByToken = new Map(); + /** * If true, cached responses are played back slowly (~ 2KiB/sec). Otherwise streaming responses are sent as fast as possible. */ @@ -139,6 +147,14 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { this.state.toolResultNormalizers.push({ toolName, normalizer }); } + /** + * Register a per-token response for the `/copilot_internal/user` endpoint. + * When a request with `Authorization: Bearer ` arrives, the matching response is returned. + */ + setCopilotUserByToken(token: string, response: CopilotUserResponse): void { + this.copilotUserByToken.set(token, response); + } + override performRequest(options: PerformRequestOptions): void { void iife(async () => { const commonResponseHeaders = { @@ -146,6 +162,18 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { }; try { + // Handle /copilot-user-config endpoint for configuring per-token user responses + if ( + options.requestOptions.path === "/copilot-user-config" && + options.requestOptions.method === "POST" + ) { + const config = JSON.parse(options.body!) as { token: string; response: CopilotUserResponse }; + this.copilotUserByToken.set(config.token, config.response); + options.onResponseStart(200, {}); + options.onResponseEnd(); + return; + } + // Handle /config endpoint for updating proxy configuration if ( options.requestOptions.path === "/config" && @@ -217,6 +245,27 @@ export class ReplayingCapiProxy extends CapturingHttpProxy { return; } + // Handle /copilot_internal/user endpoint for per-session auth + if (options.requestOptions.path === "/copilot_internal/user") { + const authHeader = options.requestOptions.headers?.["authorization"] as string | undefined; + const token = authHeader?.replace("Bearer ", ""); + const userResponse = token ? this.copilotUserByToken.get(token) : undefined; + if (userResponse) { + const headers = { + "content-type": "application/json", + ...commonResponseHeaders, + }; + options.onResponseStart(200, headers); + options.onData(Buffer.from(JSON.stringify(userResponse))); + options.onResponseEnd(); + } else { + options.onResponseStart(401, commonResponseHeaders); + options.onData(Buffer.from(JSON.stringify({ message: "Bad credentials" }))); + options.onResponseEnd(); + } + return; + } + // Handle memory endpoints - return stub responses in tests // Matches: /agents/*/memory/*/enabled, /agents/*/memory/*/recent, etc. if (options.requestOptions.path?.match(/\/agents\/.*\/memory\//)) { @@ -1113,6 +1162,20 @@ export type ToolResultNormalizer = { normalizer: (result: string) => string; }; +/** + * Response shape for the `/copilot_internal/user` endpoint. + * Used by per-session auth tests to mock GitHub identity resolution. + */ +export type CopilotUserResponse = { + login: string; + copilot_plan?: string; + endpoints?: { + api?: string; + telemetry?: string; + }; + analytics_tracking_id?: string; +}; + export type ParsedHttpExchange = { request: ChatCompletionCreateParamsBase; response: ChatCompletion | undefined; diff --git a/test/snapshots/session/should_set_model_with_reasoningeffort.yaml b/test/snapshots/session/should_set_model_with_reasoningeffort.yaml new file mode 100644 index 000000000..0e019bdad --- /dev/null +++ b/test/snapshots/session/should_set_model_with_reasoningeffort.yaml @@ -0,0 +1,8 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Run 'sleep 2 && echo done'