From a16180ebbe49091825eb05d82175c01e3d563607 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 26 Jun 2026 16:10:04 +0200 Subject: [PATCH 1/4] Add MCP OAuth lifecycle SDK support Expose host-delegated MCP OAuth handling across SDK languages, sync generated RPC and event models to the lifecycle contract, and add cross-language E2E coverage for initial auth, refresh, upscope, reauth, and cancellation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Client.cs | 10 + dotnet/src/Generated/Rpc.cs | 147 +++++- dotnet/src/Generated/SessionEvents.cs | 412 ++++++++++++++++- dotnet/src/Session.cs | 113 +++++ dotnet/src/Types.cs | 75 +++ dotnet/test/E2E/McpOAuthE2ETests.cs | 298 ++++++++++++ dotnet/test/Harness/E2ETestContext.cs | 5 + .../test/Unit/ClientSessionLifetimeTests.cs | 152 +++++++ dotnet/test/Unit/PublicDtoTests.cs | 19 + .../Unit/SessionEventSerializationTests.cs | 71 +++ go/client.go | 21 + go/client_test.go | 287 ++++++++++++ go/internal/e2e/mcp_oauth_e2e_test.go | 328 +++++++++++++ go/internal/e2e/testharness/context.go | 7 + go/rpc/zrpc.go | 145 +++++- go/rpc/zrpc_encoding.go | 87 ++++ go/rpc/zsession_encoding.go | 20 + go/rpc/zsession_events.go | 116 ++++- go/session.go | 96 ++++ go/session_test.go | 199 ++++++++ go/types.go | 54 +++ go/zsession_events.go | 25 + .../copilot/generated/AssistantIdleEvent.java | 41 ++ .../McpHeadersRefreshCompletedEvent.java | 43 ++ .../McpHeadersRefreshCompletedOutcome.java | 37 ++ .../McpHeadersRefreshRequiredEvent.java | 47 ++ .../McpHeadersRefreshRequiredReason.java | 37 ++ .../generated/McpOauthRequestReason.java | 39 ++ .../generated/McpOauthRequiredEvent.java | 4 +- .../McpOauthRequiredStaticClientConfig.java | 2 + .../McpOauthWWWAuthenticateParams.java | 2 +- .../generated/ResponseBudgetConfig.java | 29 ++ .../copilot/generated/SessionEvent.java | 6 + .../copilot/generated/SessionResumeEvent.java | 2 + .../copilot/generated/SessionStartEvent.java | 2 + .../generated/ToolExecutionStartEvent.java | 2 + .../ToolExecutionStartShellToolInfo.java | 30 ++ .../generated/UserMessageDelivery.java | 37 ++ .../copilot/generated/UserMessageEvent.java | 2 + .../copilot/generated/rpc/ModelBilling.java | 4 +- .../generated/rpc/ResponseBudgetConfig.java | 29 ++ .../copilot/generated/rpc/SessionMcpApi.java | 3 + .../generated/rpc/SessionMcpHeadersApi.java | 49 ++ ...dlePendingHeadersRefreshRequestParams.java | 34 ++ ...dlePendingHeadersRefreshRequestResult.java | 30 ++ .../rpc/SessionOptionsUpdateParams.java | 6 +- ...sionToolsUpdateSubagentSettingsParams.java | 6 +- .../rpc/SlashCommandInvocationResult.java | 2 +- .../com/github/copilot/CopilotClient.java | 21 +- .../com/github/copilot/CopilotSession.java | 76 ++++ .../github/copilot/SessionRequestBuilder.java | 6 + .../github/copilot/rpc/McpAuthHandler.java | 24 + .../github/copilot/rpc/McpAuthRequest.java | 19 + .../com/github/copilot/rpc/McpAuthResult.java | 32 ++ .../com/github/copilot/rpc/McpAuthToken.java | 13 + .../copilot/rpc/ResumeSessionConfig.java | 24 + .../com/github/copilot/rpc/SessionConfig.java | 27 ++ .../com/github/copilot/E2ETestContext.java | 12 +- .../McpAuthInterestRegistrationTest.java | 287 ++++++++++++ .../com/github/copilot/McpOAuthE2ETest.java | 291 ++++++++++++ .../copilot/SessionEventHandlingTest.java | 4 +- .../rpc/GeneratedRpcRecordsCoverageTest.java | 2 +- nodejs/src/client.ts | 18 +- nodejs/src/generated/rpc.ts | 94 +++- nodejs/src/generated/session-events.ts | 214 ++++++++- nodejs/src/session.ts | 55 ++- nodejs/src/types.ts | 77 ++++ nodejs/test/client.test.ts | 222 +++++++++ nodejs/test/e2e/harness/sdkTestContext.ts | 31 +- nodejs/test/e2e/mcp_oauth.e2e.test.ts | 311 +++++++++++++ python/copilot/__init__.py | 12 + python/copilot/client.py | 15 + python/copilot/generated/rpc.py | 173 ++++++- python/copilot/generated/session_events.py | 219 ++++++++- python/copilot/session.py | 163 +++++++ python/e2e/test_mcp_oauth_e2e.py | 266 +++++++++++ python/e2e/testharness/context.py | 9 + python/test_client.py | 309 +++++++++++++ rust/src/generated/api_types.rs | 127 +++++- rust/src/generated/rpc.rs | 53 ++- rust/src/generated/session_events.rs | 164 ++++++- rust/src/handler.rs | 95 +++- rust/src/session.rs | 115 ++++- rust/src/types.rs | 30 +- rust/tests/e2e.rs | 2 + rust/tests/e2e/mcp_oauth.rs | 430 ++++++++++++++++++ rust/tests/e2e/support.rs | 9 + rust/tests/session_test.rs | 308 ++++++++++++- test/harness/test-mcp-oauth-server.mjs | 305 +++++++++++++ 89 files changed, 7784 insertions(+), 92 deletions(-) create mode 100644 dotnet/test/E2E/McpOAuthE2ETests.cs create mode 100644 go/internal/e2e/mcp_oauth_e2e_test.go create mode 100644 java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java create mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java create mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java create mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java create mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java create mode 100644 java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java create mode 100644 java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java create mode 100644 java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java create mode 100644 java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java create mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java create mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java create mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java create mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java create mode 100644 java/src/main/java/com/github/copilot/rpc/McpAuthHandler.java create mode 100644 java/src/main/java/com/github/copilot/rpc/McpAuthRequest.java create mode 100644 java/src/main/java/com/github/copilot/rpc/McpAuthResult.java create mode 100644 java/src/main/java/com/github/copilot/rpc/McpAuthToken.java create mode 100644 java/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java create mode 100644 java/src/test/java/com/github/copilot/McpOAuthE2ETest.java create mode 100644 nodejs/test/e2e/mcp_oauth.e2e.test.ts create mode 100644 python/e2e/test_mcp_oauth_e2e.py create mode 100644 rust/tests/e2e/mcp_oauth.rs create mode 100644 test/harness/test-mcp-oauth-server.mjs diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index a67eb96817..9fbe8c5a72 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -630,6 +630,7 @@ private CopilotSession InitializeSession( this); session.RegisterTools(config.Tools ?? []); session.RegisterPermissionHandler(config.OnPermissionRequest); + session.RegisterMcpAuthHandler(config.OnMcpAuthRequest); session.RegisterCommands(config.Commands); session.RegisterElicitationHandler(config.OnElicitationRequest); session.RegisterExitPlanModeHandler(config.OnExitPlanModeRequest); @@ -1080,6 +1081,11 @@ public async Task CreateSessionAsync(SessionConfig config, Cance $"session.create returned sessionId {response.SessionId} but the caller requested {localSessionId}."); } + if (config.OnMcpAuthRequest is not null) + { + await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken); + } + session.WorkspacePath = response.WorkspacePath; session.SetCapabilities(response.Capabilities); session.SetOpenCanvases(response.OpenCanvases); @@ -1166,6 +1172,10 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes transformCallbacks, hasHooks, "CopilotClient.ResumeSessionAsync"); + if (config.OnMcpAuthRequest is not null) + { + await session.Rpc.EventLog.RegisterInterestAsync("mcp.oauth_required", cancellationToken); + } try { diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 3a9fcf9cde..c5724b2217 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -149,6 +149,10 @@ public sealed class ModelBillingTokenPrices /// Billing information. public sealed class ModelBilling { + /// Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. + [JsonPropertyName("discountPercent")] + public int? DiscountPercent { get; set; } + /// Billing cost multiplier relative to the base rate. [JsonPropertyName("multiplier")] public double? Multiplier { get; set; } @@ -5612,11 +5616,6 @@ public partial class McpOauthPendingRequestResponseToken : McpOauthPendingReques [JsonPropertyName("expiresIn")] public long? ExpiresIn { get; set; } - /// Refresh token supplied by the host, if available. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("refreshToken")] - public string? RefreshToken { get; set; } - /// OAuth token type. Defaults to Bearer when omitted. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("tokenType")] @@ -5704,6 +5703,70 @@ internal sealed class McpOauthLoginRequest public string SessionId { get; set; } = string.Empty; } +/// Indicates whether the pending MCP headers refresh response was accepted. +[Experimental(Diagnostics.Experimental)] +public sealed class McpHeadersHandlePendingHeadersRefreshRequestResult +{ + /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. + [JsonPropertyName("success")] + public bool Success { get; set; } +} + +/// Host response: supply dynamic headers or decline this refresh. +/// Polymorphic base type discriminated by kind. +[Experimental(Diagnostics.Experimental)] +[JsonPolymorphic( + TypeDiscriminatorPropertyName = "kind", + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestHeaders), "headers")] +[JsonDerivedType(typeof(McpHeadersHandlePendingHeadersRefreshRequestNone), "none")] +public partial class McpHeadersHandlePendingHeadersRefreshRequest +{ + /// The type discriminator. + [JsonPropertyName("kind")] + public virtual string Kind { get; set; } = string.Empty; +} + + +/// The headers variant of . +[Experimental(Diagnostics.Experimental)] +public partial class McpHeadersHandlePendingHeadersRefreshRequestHeaders : McpHeadersHandlePendingHeadersRefreshRequest +{ + /// + [JsonIgnore] + public override string Kind => "headers"; + + /// Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers. + [JsonPropertyName("headers")] + public required IDictionary Headers { get; set; } +} + +/// The none variant of . +[Experimental(Diagnostics.Experimental)] +public partial class McpHeadersHandlePendingHeadersRefreshRequestNone : McpHeadersHandlePendingHeadersRefreshRequest +{ + /// + [JsonIgnore] + public override string Kind => "none"; +} + +/// MCP headers refresh request id and the host response. +[Experimental(Diagnostics.Experimental)] +internal sealed class McpHeadersHandlePendingHeadersRefreshRequestRequest +{ + /// Headers refresh request identifier from mcp.headers_refresh_required. + [JsonPropertyName("requestId")] + public string RequestId { get; set; } = string.Empty; + + /// Host response: supply dynamic headers or decline this refresh. + [JsonPropertyName("result")] + public McpHeadersHandlePendingHeadersRefreshRequest Result { get => field ??= new(); set; } + + /// Target session identifier. + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + /// Schema for the `McpAppsResourceContent` type. [Experimental(Diagnostics.Experimental)] public sealed class McpAppsResourceContent @@ -6552,6 +6615,10 @@ internal sealed class SessionUpdateOptionsParams [JsonPropertyName("agentContext")] public string? AgentContext { get; set; } + /// Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. + [JsonPropertyName("allowAllMcpServerInstructions")] + public bool? AllowAllMcpServerInstructions { get; set; } + /// Whether to disable the `ask_user` tool (encourages autonomous behavior). [JsonPropertyName("askUserDisabled")] public bool? AskUserDisabled { get; set; } @@ -6696,6 +6763,10 @@ internal sealed class SessionUpdateOptionsParams [JsonPropertyName("reasoningSummary")] public OptionsUpdateReasoningSummary? ReasoningSummary { get; set; } + /// Optional experimental response budget limits. Pass null to clear the response budget. + [JsonPropertyName("responseBudget")] + public ResponseBudgetConfig? ResponseBudget { get; set; } + /// Whether the session is running in an interactive UI. [JsonPropertyName("runningInInteractiveMode")] public bool? RunningInInteractiveMode { get; set; } @@ -7207,6 +7278,14 @@ public sealed class UpdateSubagentSettingsRequestSubagents /// Names of subagents the user has turned off; they cannot be dispatched. [JsonPropertyName("disabledSubagents")] public IList? DisabledSubagents { get; set; } + + /// Maximum number of subagents that can run concurrently; applies to usage-based billing users only. + [JsonPropertyName("maxConcurrency")] + public int? MaxConcurrency { get; set; } + + /// Maximum subagent nesting depth; applies to usage-based billing users only. + [JsonPropertyName("maxDepth")] + public int? MaxDepth { get; set; } } /// Subagent settings to apply to the current session. @@ -7327,7 +7406,7 @@ internal sealed class CommandsListRequestWithSession public string SessionId { get; set; } = string.Empty; } -/// Result of invoking the slash command (text output, prompt to send to the agent, or completion). +/// Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). /// Polymorphic base type discriminated by kind. [Experimental(Diagnostics.Experimental)] [JsonPolymorphic( @@ -19139,6 +19218,12 @@ public async Task IsServerRunningAsync(string serverNa Interlocked.CompareExchange(ref field, new(_session), null) ?? field; + /// Headers APIs. + public McpHeadersApi Headers => + field ?? + Interlocked.CompareExchange(ref field, new(_session), null) ?? + field; + /// Apps APIs. public McpAppsApi Apps => field ?? @@ -19207,6 +19292,33 @@ public async Task LoginAsync(string serverName, bool? force } } +/// Provides session-scoped McpHeaders APIs. +[Experimental(Diagnostics.Experimental)] +public sealed class McpHeadersApi +{ + private readonly CopilotSession _session; + + internal McpHeadersApi(CopilotSession session) + { + _session = session; + } + + /// Responds to a pending MCP dynamic headers refresh request. Hosts that subscribe to `mcp.headers_refresh_required` use this to provide short-lived per-server headers or to indicate that no dynamic headers are available for this refresh. + /// Headers refresh request identifier from mcp.headers_refresh_required. + /// Host response: supply dynamic headers or decline this refresh. + /// The to monitor for cancellation requests. The default is . + /// Indicates whether the pending MCP headers refresh response was accepted. + public async Task HandlePendingHeadersRefreshRequestAsync(string requestId, McpHeadersHandlePendingHeadersRefreshRequest result, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(requestId); + ArgumentNullException.ThrowIfNull(result); + _session.ThrowIfDisposed(); + + var request = new McpHeadersHandlePendingHeadersRefreshRequestRequest { SessionId = _session.SessionId, RequestId = requestId, Result = result }; + return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.mcp.headers.handlePendingHeadersRefreshRequest", [request], cancellationToken); + } +} + /// Provides session-scoped McpApps APIs. [Experimental(Diagnostics.Experimental)] public sealed class McpAppsApi @@ -19407,6 +19519,7 @@ internal OptionsApi(CopilotSession session) /// Resolved sandbox configuration. /// Whether interactive shell sessions are logged. /// How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). + /// Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. /// Additional directories to search for skills. /// Skill IDs that should be excluded from this session. /// Whether to discover custom instructions on demand after successful file views (AGENTS.md / CLAUDE.md / .github/copilot-instructions.md surfacing). Combined with `skipCustomInstructions` and the runtime-side `ON_DEMAND_INSTRUCTIONS` feature flag. @@ -19436,13 +19549,14 @@ internal OptionsApi(CopilotSession session) /// Whether to enable cross-session store writes and reads. /// Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. /// Context tier for models with tiered pricing. The session uses this to derive effective `modelCapabilitiesOverrides` so compaction, truncation, token display, and request limits honor the selected tier. + /// Optional experimental response budget limits. Pass null to clear the response budget. /// The to monitor for cancellation requests. The default is . /// Indicates whether the session options patch was applied successfully. - public async Task UpdateAsync(string? model = null, ModelCapabilitiesOverride? modelCapabilitiesOverrides = null, string? reasoningEffort = null, OptionsUpdateReasoningSummary? reasoningSummary = null, string? clientName = null, string? lspClientName = null, string? integrationId = null, IDictionary? featureFlags = null, bool? isExperimentalMode = null, ProviderConfig? provider = null, CapiSessionOptions? capi = null, string? workingDirectory = null, IList? availableTools = null, IList? excludedTools = null, OptionsUpdateToolFilterPrecedence? toolFilterPrecedence = null, bool? enableScriptSafety = null, string? shellInitProfile = null, IList? shellProcessFlags = null, SandboxConfig? sandboxConfig = null, bool? logInteractiveShells = null, OptionsUpdateEnvValueMode? envValueMode = null, IList? skillDirectories = null, IList? disabledSkills = null, bool? enableOnDemandInstructionDiscovery = null, long? maxInlineBinaryBytes = null, IList? installedPlugins = null, bool? customAgentsLocalOnly = null, bool? suppressCustomAgentPrompt = null, bool? skipCustomInstructions = null, IList? disabledInstructionSources = null, bool? coauthorEnabled = null, string? trajectoryFile = null, bool? enableStreaming = null, string? copilotUrl = null, bool? askUserDisabled = null, bool? continueOnAutoMode = null, bool? runningInInteractiveMode = null, bool? enableReasoningSummaries = null, string? agentContext = null, string? eventsLogDirectory = null, IList? additionalContentExclusionPolicies = null, bool? manageScheduleEnabled = null, IList? sessionCapabilities = null, bool? skipEmbeddingRetrieval = null, string? organizationCustomInstructions = null, bool? enableFileHooks = null, bool? enableHostGitOperations = null, bool? enableSessionStore = null, bool? enableSkills = null, OptionsUpdateContextTier? contextTier = null, CancellationToken cancellationToken = default) + public async Task UpdateAsync(string? model = null, ModelCapabilitiesOverride? modelCapabilitiesOverrides = null, string? reasoningEffort = null, OptionsUpdateReasoningSummary? reasoningSummary = null, string? clientName = null, string? lspClientName = null, string? integrationId = null, IDictionary? featureFlags = null, bool? isExperimentalMode = null, ProviderConfig? provider = null, CapiSessionOptions? capi = null, string? workingDirectory = null, IList? availableTools = null, IList? excludedTools = null, OptionsUpdateToolFilterPrecedence? toolFilterPrecedence = null, bool? enableScriptSafety = null, string? shellInitProfile = null, IList? shellProcessFlags = null, SandboxConfig? sandboxConfig = null, bool? logInteractiveShells = null, OptionsUpdateEnvValueMode? envValueMode = null, bool? allowAllMcpServerInstructions = null, IList? skillDirectories = null, IList? disabledSkills = null, bool? enableOnDemandInstructionDiscovery = null, long? maxInlineBinaryBytes = null, IList? installedPlugins = null, bool? customAgentsLocalOnly = null, bool? suppressCustomAgentPrompt = null, bool? skipCustomInstructions = null, IList? disabledInstructionSources = null, bool? coauthorEnabled = null, string? trajectoryFile = null, bool? enableStreaming = null, string? copilotUrl = null, bool? askUserDisabled = null, bool? continueOnAutoMode = null, bool? runningInInteractiveMode = null, bool? enableReasoningSummaries = null, string? agentContext = null, string? eventsLogDirectory = null, IList? additionalContentExclusionPolicies = null, bool? manageScheduleEnabled = null, IList? sessionCapabilities = null, bool? skipEmbeddingRetrieval = null, string? organizationCustomInstructions = null, bool? enableFileHooks = null, bool? enableHostGitOperations = null, bool? enableSessionStore = null, bool? enableSkills = null, OptionsUpdateContextTier? contextTier = null, ResponseBudgetConfig? responseBudget = null, CancellationToken cancellationToken = default) { _session.ThrowIfDisposed(); - var request = new SessionUpdateOptionsParams { SessionId = _session.SessionId, Model = model, ModelCapabilitiesOverrides = modelCapabilitiesOverrides, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, ClientName = clientName, LspClientName = lspClientName, IntegrationId = integrationId, FeatureFlags = featureFlags, IsExperimentalMode = isExperimentalMode, Provider = provider, Capi = capi, WorkingDirectory = workingDirectory, AvailableTools = availableTools, ExcludedTools = excludedTools, ToolFilterPrecedence = toolFilterPrecedence, EnableScriptSafety = enableScriptSafety, ShellInitProfile = shellInitProfile, ShellProcessFlags = shellProcessFlags, SandboxConfig = sandboxConfig, LogInteractiveShells = logInteractiveShells, EnvValueMode = envValueMode, SkillDirectories = skillDirectories, DisabledSkills = disabledSkills, EnableOnDemandInstructionDiscovery = enableOnDemandInstructionDiscovery, MaxInlineBinaryBytes = maxInlineBinaryBytes, InstalledPlugins = installedPlugins, CustomAgentsLocalOnly = customAgentsLocalOnly, SuppressCustomAgentPrompt = suppressCustomAgentPrompt, SkipCustomInstructions = skipCustomInstructions, DisabledInstructionSources = disabledInstructionSources, CoauthorEnabled = coauthorEnabled, TrajectoryFile = trajectoryFile, EnableStreaming = enableStreaming, CopilotUrl = copilotUrl, AskUserDisabled = askUserDisabled, ContinueOnAutoMode = continueOnAutoMode, RunningInInteractiveMode = runningInInteractiveMode, EnableReasoningSummaries = enableReasoningSummaries, AgentContext = agentContext, EventsLogDirectory = eventsLogDirectory, AdditionalContentExclusionPolicies = additionalContentExclusionPolicies, ManageScheduleEnabled = manageScheduleEnabled, SessionCapabilities = sessionCapabilities, SkipEmbeddingRetrieval = skipEmbeddingRetrieval, OrganizationCustomInstructions = organizationCustomInstructions, EnableFileHooks = enableFileHooks, EnableHostGitOperations = enableHostGitOperations, EnableSessionStore = enableSessionStore, EnableSkills = enableSkills, ContextTier = contextTier }; + var request = new SessionUpdateOptionsParams { SessionId = _session.SessionId, Model = model, ModelCapabilitiesOverrides = modelCapabilitiesOverrides, ReasoningEffort = reasoningEffort, ReasoningSummary = reasoningSummary, ClientName = clientName, LspClientName = lspClientName, IntegrationId = integrationId, FeatureFlags = featureFlags, IsExperimentalMode = isExperimentalMode, Provider = provider, Capi = capi, WorkingDirectory = workingDirectory, AvailableTools = availableTools, ExcludedTools = excludedTools, ToolFilterPrecedence = toolFilterPrecedence, EnableScriptSafety = enableScriptSafety, ShellInitProfile = shellInitProfile, ShellProcessFlags = shellProcessFlags, SandboxConfig = sandboxConfig, LogInteractiveShells = logInteractiveShells, EnvValueMode = envValueMode, AllowAllMcpServerInstructions = allowAllMcpServerInstructions, SkillDirectories = skillDirectories, DisabledSkills = disabledSkills, EnableOnDemandInstructionDiscovery = enableOnDemandInstructionDiscovery, MaxInlineBinaryBytes = maxInlineBinaryBytes, InstalledPlugins = installedPlugins, CustomAgentsLocalOnly = customAgentsLocalOnly, SuppressCustomAgentPrompt = suppressCustomAgentPrompt, SkipCustomInstructions = skipCustomInstructions, DisabledInstructionSources = disabledInstructionSources, CoauthorEnabled = coauthorEnabled, TrajectoryFile = trajectoryFile, EnableStreaming = enableStreaming, CopilotUrl = copilotUrl, AskUserDisabled = askUserDisabled, ContinueOnAutoMode = continueOnAutoMode, RunningInInteractiveMode = runningInInteractiveMode, EnableReasoningSummaries = enableReasoningSummaries, AgentContext = agentContext, EventsLogDirectory = eventsLogDirectory, AdditionalContentExclusionPolicies = additionalContentExclusionPolicies, ManageScheduleEnabled = manageScheduleEnabled, SessionCapabilities = sessionCapabilities, SkipEmbeddingRetrieval = skipEmbeddingRetrieval, OrganizationCustomInstructions = organizationCustomInstructions, EnableFileHooks = enableFileHooks, EnableHostGitOperations = enableHostGitOperations, EnableSessionStore = enableSessionStore, EnableSkills = enableSkills, ContextTier = contextTier, ResponseBudget = responseBudget }; return await CopilotClient.InvokeRpcAsync(_session.Rpc, "session.options.update", [request], cancellationToken); } } @@ -19630,7 +19744,7 @@ public async Task ListAsync(CommandsListRequest? request = null, Ca /// Command name. Leading slashes are stripped and the name is matched case-insensitively. /// Raw input after the command name. /// The to monitor for cancellation requests. The default is . - /// Result of invoking the slash command (text output, prompt to send to the agent, or completion). + /// Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). public async Task InvokeAsync(string name, string? input = null, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(name); @@ -20957,6 +21071,8 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.AbortData), TypeInfoPropertyName = "SessionEventsAbortData")] [JsonSerializable(typeof(GitHub.Copilot.AbortEvent), TypeInfoPropertyName = "SessionEventsAbortEvent")] [JsonSerializable(typeof(GitHub.Copilot.AbortReason), TypeInfoPropertyName = "SessionEventsAbortReason")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantIdleData), TypeInfoPropertyName = "SessionEventsAssistantIdleData")] +[JsonSerializable(typeof(GitHub.Copilot.AssistantIdleEvent), TypeInfoPropertyName = "SessionEventsAssistantIdleEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantIntentData), TypeInfoPropertyName = "SessionEventsAssistantIntentData")] [JsonSerializable(typeof(GitHub.Copilot.AssistantIntentEvent), TypeInfoPropertyName = "SessionEventsAssistantIntentEvent")] [JsonSerializable(typeof(GitHub.Copilot.AssistantMessageData), TypeInfoPropertyName = "SessionEventsAssistantMessageData")] @@ -21068,9 +21184,16 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.McpAppToolCallCompleteEvent), TypeInfoPropertyName = "SessionEventsMcpAppToolCallCompleteEvent")] [JsonSerializable(typeof(GitHub.Copilot.McpAppToolCallCompleteToolMeta), TypeInfoPropertyName = "SessionEventsMcpAppToolCallCompleteToolMeta")] [JsonSerializable(typeof(GitHub.Copilot.McpAppToolCallCompleteToolMetaUI), TypeInfoPropertyName = "SessionEventsMcpAppToolCallCompleteToolMetaUI")] +[JsonSerializable(typeof(GitHub.Copilot.McpHeadersRefreshCompletedData), TypeInfoPropertyName = "SessionEventsMcpHeadersRefreshCompletedData")] +[JsonSerializable(typeof(GitHub.Copilot.McpHeadersRefreshCompletedEvent), TypeInfoPropertyName = "SessionEventsMcpHeadersRefreshCompletedEvent")] +[JsonSerializable(typeof(GitHub.Copilot.McpHeadersRefreshCompletedOutcome), TypeInfoPropertyName = "SessionEventsMcpHeadersRefreshCompletedOutcome")] +[JsonSerializable(typeof(GitHub.Copilot.McpHeadersRefreshRequiredData), TypeInfoPropertyName = "SessionEventsMcpHeadersRefreshRequiredData")] +[JsonSerializable(typeof(GitHub.Copilot.McpHeadersRefreshRequiredEvent), TypeInfoPropertyName = "SessionEventsMcpHeadersRefreshRequiredEvent")] +[JsonSerializable(typeof(GitHub.Copilot.McpHeadersRefreshRequiredReason), TypeInfoPropertyName = "SessionEventsMcpHeadersRefreshRequiredReason")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthCompletedData), TypeInfoPropertyName = "SessionEventsMcpOauthCompletedData")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthCompletedEvent), TypeInfoPropertyName = "SessionEventsMcpOauthCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthCompletionOutcome), TypeInfoPropertyName = "SessionEventsMcpOauthCompletionOutcome")] +[JsonSerializable(typeof(GitHub.Copilot.McpOauthRequestReason), TypeInfoPropertyName = "SessionEventsMcpOauthRequestReason")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthRequiredData), TypeInfoPropertyName = "SessionEventsMcpOauthRequiredData")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthRequiredEvent), TypeInfoPropertyName = "SessionEventsMcpOauthRequiredEvent")] [JsonSerializable(typeof(GitHub.Copilot.McpOauthRequiredStaticClientConfig), TypeInfoPropertyName = "SessionEventsMcpOauthRequiredStaticClientConfig")] @@ -21128,6 +21251,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.PersistedBinaryResult), TypeInfoPropertyName = "SessionEventsPersistedBinaryResult")] [JsonSerializable(typeof(GitHub.Copilot.PlanChangedOperation), TypeInfoPropertyName = "SessionEventsPlanChangedOperation")] [JsonSerializable(typeof(GitHub.Copilot.ReasoningSummary), TypeInfoPropertyName = "SessionEventsReasoningSummary")] +[JsonSerializable(typeof(GitHub.Copilot.ResponseBudgetConfig), TypeInfoPropertyName = "SessionEventsResponseBudgetConfig")] [JsonSerializable(typeof(GitHub.Copilot.SamplingCompletedData), TypeInfoPropertyName = "SessionEventsSamplingCompletedData")] [JsonSerializable(typeof(GitHub.Copilot.SamplingCompletedEvent), TypeInfoPropertyName = "SessionEventsSamplingCompletedEvent")] [JsonSerializable(typeof(GitHub.Copilot.SamplingRequestedData), TypeInfoPropertyName = "SessionEventsSamplingRequestedData")] @@ -21202,6 +21326,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionProgressEvent), TypeInfoPropertyName = "SessionEventsToolExecutionProgressEvent")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionStartData), TypeInfoPropertyName = "SessionEventsToolExecutionStartData")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionStartEvent), TypeInfoPropertyName = "SessionEventsToolExecutionStartEvent")] +[JsonSerializable(typeof(GitHub.Copilot.ToolExecutionStartShellToolInfo), TypeInfoPropertyName = "SessionEventsToolExecutionStartShellToolInfo")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionStartToolDescription), TypeInfoPropertyName = "SessionEventsToolExecutionStartToolDescription")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionStartToolDescriptionMeta), TypeInfoPropertyName = "SessionEventsToolExecutionStartToolDescriptionMeta")] [JsonSerializable(typeof(GitHub.Copilot.ToolExecutionStartToolDescriptionMetaUI), TypeInfoPropertyName = "SessionEventsToolExecutionStartToolDescriptionMetaUI")] @@ -21214,6 +21339,7 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(GitHub.Copilot.UserInputRequestedEvent), TypeInfoPropertyName = "SessionEventsUserInputRequestedEvent")] [JsonSerializable(typeof(GitHub.Copilot.UserMessageAgentMode), TypeInfoPropertyName = "SessionEventsUserMessageAgentMode")] [JsonSerializable(typeof(GitHub.Copilot.UserMessageData), TypeInfoPropertyName = "SessionEventsUserMessageData")] +[JsonSerializable(typeof(GitHub.Copilot.UserMessageDelivery), TypeInfoPropertyName = "SessionEventsUserMessageDelivery")] [JsonSerializable(typeof(GitHub.Copilot.UserMessageEvent), TypeInfoPropertyName = "SessionEventsUserMessageEvent")] [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApproval), TypeInfoPropertyName = "SessionEventsUserToolSessionApproval")] [JsonSerializable(typeof(GitHub.Copilot.UserToolSessionApprovalCommands), TypeInfoPropertyName = "SessionEventsUserToolSessionApprovalCommands")] @@ -21387,6 +21513,9 @@ public static void RegisterClientGlobalApiHandlers(JsonRpc rpc, ClientGlobalApiH [JsonSerializable(typeof(McpExecuteSamplingRequest))] [JsonSerializable(typeof(McpExecuteSamplingResult))] [JsonSerializable(typeof(McpFilteredServer))] +[JsonSerializable(typeof(McpHeadersHandlePendingHeadersRefreshRequest))] +[JsonSerializable(typeof(McpHeadersHandlePendingHeadersRefreshRequestRequest))] +[JsonSerializable(typeof(McpHeadersHandlePendingHeadersRefreshRequestResult))] [JsonSerializable(typeof(McpHostState))] [JsonSerializable(typeof(McpIsServerRunningRequest))] [JsonSerializable(typeof(McpIsServerRunningResult))] diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index f1765e5c44..381780dd57 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -25,6 +25,7 @@ namespace GitHub.Copilot; TypeDiscriminatorPropertyName = "type", IgnoreUnrecognizedTypeDiscriminators = true)] [JsonDerivedType(typeof(AbortEvent), "abort")] +[JsonDerivedType(typeof(AssistantIdleEvent), "assistant.idle")] [JsonDerivedType(typeof(AssistantIntentEvent), "assistant.intent")] [JsonDerivedType(typeof(AssistantMessageEvent), "assistant.message")] [JsonDerivedType(typeof(AssistantMessageDeltaEvent), "assistant.message_delta")] @@ -52,6 +53,8 @@ namespace GitHub.Copilot; [JsonDerivedType(typeof(HookProgressEvent), "hook.progress")] [JsonDerivedType(typeof(HookStartEvent), "hook.start")] [JsonDerivedType(typeof(McpAppToolCallCompleteEvent), "mcp_app.tool_call_complete")] +[JsonDerivedType(typeof(McpHeadersRefreshCompletedEvent), "mcp.headers_refresh_completed")] +[JsonDerivedType(typeof(McpHeadersRefreshRequiredEvent), "mcp.headers_refresh_required")] [JsonDerivedType(typeof(McpOauthCompletedEvent), "mcp.oauth_completed")] [JsonDerivedType(typeof(McpOauthRequiredEvent), "mcp.oauth_required")] [JsonDerivedType(typeof(ModelCallFailureEvent), "model.call_failure")] @@ -655,6 +658,19 @@ public sealed partial class AssistantTurnEndEvent : SessionEvent public required AssistantTurnEndData Data { get; set; } } +/// Payload emitted whenever the main agent's processing loop goes idle, including while related background work (running agents or in-flight attached shell commands) is still pending and the session-level idle event is therefore deferred. +/// Represents the assistant.idle event. +public sealed partial class AssistantIdleEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "assistant.idle"; + + /// The assistant.idle event payload. + [JsonPropertyName("data")] + public required AssistantIdleData Data { get; set; } +} + /// LLM API call usage metrics including tokens, costs, quotas, and billing information. /// Represents the assistant.usage event. public sealed partial class AssistantUsageEvent : SessionEvent @@ -1046,6 +1062,32 @@ public sealed partial class McpOauthCompletedEvent : SessionEvent public required McpOauthCompletedData Data { get; set; } } +/// Dynamic headers refresh request for a remote MCP server. +/// Represents the mcp.headers_refresh_required event. +public sealed partial class McpHeadersRefreshRequiredEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "mcp.headers_refresh_required"; + + /// The mcp.headers_refresh_required event payload. + [JsonPropertyName("data")] + public required McpHeadersRefreshRequiredData Data { get; set; } +} + +/// MCP headers refresh request completion notification. +/// Represents the mcp.headers_refresh_completed event. +public sealed partial class McpHeadersRefreshCompletedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "mcp.headers_refresh_completed"; + + /// The mcp.headers_refresh_completed event payload. + [JsonPropertyName("data")] + public required McpHeadersRefreshCompletedData Data { get; set; } +} + /// Opaque custom notification data. Consumers may branch on source and name, but payload semantics are source-defined. /// Represents the session.custom_notification event. public sealed partial class SessionCustomNotificationEvent : SessionEvent @@ -1449,6 +1491,11 @@ public sealed partial class SessionStartData [JsonPropertyName("remoteSteerable")] public bool? RemoteSteerable { get; set; } + /// Response budget limits configured at session creation time, if any. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("responseBudget")] + public ResponseBudgetConfig? ResponseBudget { get; set; } + /// Model selected at session creation time, if any. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("selectedModel")] @@ -1514,6 +1561,11 @@ public sealed partial class SessionResumeData [JsonPropertyName("remoteSteerable")] public bool? RemoteSteerable { get; set; } + /// Response budget limits currently configured at resume time; null when no budget is active. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("responseBudget")] + public ResponseBudgetConfig? ResponseBudget { get; set; } + /// ISO 8601 timestamp when the session was resumed. [JsonPropertyName("resumeTime")] public required DateTimeOffset ResumeTime { get; set; } @@ -2202,6 +2254,11 @@ public sealed partial class UserMessageData [JsonPropertyName("content")] public required string Content { get; set; } + /// How this message was delivered to the agentic loop relative to loop state (idle-start vs. steering/queued while busy). The timing axis; combine with `source` (origin) for the full picture. Used for telemetry attribution. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("delivery")] + public UserMessageDelivery? Delivery { get; set; } + /// CAPI interaction ID for correlating this user message with its turn. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("interactionId")] @@ -2426,6 +2483,15 @@ public sealed partial class AssistantTurnEndData public required string TurnId { get; set; } } +/// Payload emitted whenever the main agent's processing loop goes idle, including while related background work (running agents or in-flight attached shell commands) is still pending and the session-level idle event is therefore deferred. +public sealed partial class AssistantIdleData +{ + /// True when the preceding agentic loop was cancelled via abort signal. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("aborted")] + public bool? Aborted { get; set; } +} + /// LLM API call usage metrics including tokens, costs, quotas, and billing information. public sealed partial class AssistantUsageData { @@ -2676,6 +2742,11 @@ public sealed partial class ToolExecutionStartData [JsonPropertyName("parentToolCallId")] public string? ParentToolCallId { get; set; } + /// Shell-tool path hints derived from the command at start time for shell tools (bash/powershell/local_shell). Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. Absent for non-shell tools. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("shellToolInfo")] + public ToolExecutionStartShellToolInfo? ShellToolInfo { get; set; } + /// Unique identifier for this tool call. [JsonPropertyName("toolCallId")] public required string ToolCallId { get; set; } @@ -3244,6 +3315,10 @@ public sealed partial class SamplingCompletedData /// OAuth authentication request for an MCP server. public sealed partial class McpOauthRequiredData { + /// Why the runtime is requesting host-provided OAuth credentials. + [JsonPropertyName("reason")] + public required McpOauthRequestReason Reason { get; set; } + /// Unique identifier for this OAuth request; used to respond via session.mcp.oauth.handlePendingRequest. [JsonPropertyName("requestId")] public required string RequestId { get; set; } @@ -3284,6 +3359,38 @@ public sealed partial class McpOauthCompletedData public required string RequestId { get; set; } } +/// Dynamic headers refresh request for a remote MCP server. +public sealed partial class McpHeadersRefreshRequiredData +{ + /// Why dynamic headers are being requested. + [JsonPropertyName("reason")] + public required McpHeadersRefreshRequiredReason Reason { get; set; } + + /// Unique identifier for this headers refresh request; used to respond via session.mcp.headers.handlePendingHeadersRefreshRequest(). + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } + + /// Display name of the remote MCP server requesting headers. + [JsonPropertyName("serverName")] + public required string ServerName { get; set; } + + /// URL of the remote MCP server requesting headers. + [JsonPropertyName("serverUrl")] + public required string ServerUrl { get; set; } +} + +/// MCP headers refresh request completion notification. +public sealed partial class McpHeadersRefreshCompletedData +{ + /// How the pending MCP headers refresh request resolved. + [JsonPropertyName("outcome")] + public required McpHeadersRefreshCompletedOutcome Outcome { get; set; } + + /// Request ID of the resolved headers refresh request. + [JsonPropertyName("requestId")] + public required string RequestId { get; set; } +} + /// Opaque custom notification data. Consumers may branch on source and name, but payload semantics are source-defined. public sealed partial class SessionCustomNotificationData { @@ -3800,6 +3907,21 @@ public sealed partial class WorkingDirectoryContext public string? RepositoryHost { get; set; } } +/// Optional response budget limits. +/// Nested data type for ResponseBudgetConfig. +public sealed partial class ResponseBudgetConfig +{ + /// Maximum AI Credits allowed while responding to one top-level user message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("maxAiCredits")] + public double? MaxAiCredits { get; set; } + + /// Maximum model-call iterations allowed while responding to one top-level user message. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("maxModelIterations")] + public long? MaxModelIterations { get; set; } +} + /// Repository context for the handed-off session. /// Nested data type for HandoffRepository. public sealed partial class HandoffRepository @@ -4633,6 +4755,19 @@ public sealed partial class ModelCallFailureRequestFingerprint public required long ToolResultMessageCount { get; set; } } +/// Shell-aware path hints for a shell tool's command, captured at start time so consumers can snapshot a file's pre-image before the tool runs. +/// Nested data type for ToolExecutionStartShellToolInfo. +public sealed partial class ToolExecutionStartShellToolInfo +{ + /// Whether the command includes a file write redirection (e.g., > or >>). + [JsonPropertyName("hasWriteFileRedirection")] + public required bool HasWriteFileRedirection { get; set; } + + /// File paths the command may read or write, derived from the command at start time. Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. + [JsonPropertyName("possiblePaths")] + public required string[] PossiblePaths { get; set; } +} + /// Schema for the `ToolExecutionStartToolDescriptionMetaUI` type. /// Nested data type for ToolExecutionStartToolDescriptionMetaUI. public sealed partial class ToolExecutionStartToolDescriptionMetaUI @@ -6620,6 +6755,11 @@ public sealed partial class McpOauthRequiredStaticClientConfig [JsonPropertyName("clientId")] public required string ClientId { get; set; } + /// Optional OAuth client secret for confidential static clients, when the runtime can resolve one. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("clientSecret")] + public string? ClientSecret { get; set; } + /// Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("grantType")] @@ -6640,9 +6780,10 @@ public sealed partial class McpOauthWWWAuthenticateParams [JsonPropertyName("error")] public string? Error { get; set; } - /// Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter. + /// Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter, if present. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("resourceMetadataUrl")] - public required string ResourceMetadataUrl { get; set; } + public string? ResourceMetadataUrl { get; set; } /// Requested OAuth scopes from the WWW-Authenticate scope parameter, if present. [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -7729,6 +7870,70 @@ public override void Write(Utf8JsonWriter writer, AttachmentGitHubReferenceType } } +/// How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct UserMessageDelivery : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public UserMessageDelivery(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Delivered while the loop was idle; starts its own run immediately (a human's fresh turn, or a system notification waking an idle agent). + public static UserMessageDelivery Idle { get; } = new("idle"); + + /// Injected into the current in-flight run while the agent was busy (immediate mode). + public static UserMessageDelivery Steering { get; } = new("steering"); + + /// Enqueued while the agent was busy; processed as its own run afterward. + public static UserMessageDelivery Queued { get; } = new("queued"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(UserMessageDelivery left, UserMessageDelivery right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(UserMessageDelivery left, UserMessageDelivery right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is UserMessageDelivery other && Equals(other); + + /// + public bool Equals(UserMessageDelivery other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override UserMessageDelivery Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, UserMessageDelivery value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(UserMessageDelivery)); + } + } +} + /// The system that produced a citation. [Experimental(Diagnostics.Experimental)] [JsonConverter(typeof(Converter))] @@ -9035,6 +9240,73 @@ public override void Write(Utf8JsonWriter writer, ElicitationCompletedAction val } } +/// Reason the runtime is requesting host-provided MCP OAuth credentials. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpOauthRequestReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpOauthRequestReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// Initial credentials are required before connecting to the MCP server. + public static McpOauthRequestReason Initial { get; } = new("initial"); + + /// The current host-provided credential was rejected and a replacement is requested. + public static McpOauthRequestReason Refresh { get; } = new("refresh"); + + /// The server requires a new host authorization flow before continuing. + public static McpOauthRequestReason Reauth { get; } = new("reauth"); + + /// The server requires a credential with additional scope or audience. + public static McpOauthRequestReason Upscope { get; } = new("upscope"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpOauthRequestReason left, McpOauthRequestReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpOauthRequestReason left, McpOauthRequestReason right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpOauthRequestReason other && Equals(other); + + /// + public bool Equals(McpOauthRequestReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override McpOauthRequestReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpOauthRequestReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpOauthRequestReason)); + } + } +} + /// How the pending MCP OAuth request was completed. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -9096,6 +9368,134 @@ public override void Write(Utf8JsonWriter writer, McpOauthCompletionOutcome valu } } +/// Why dynamic headers are being requested. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpHeadersRefreshRequiredReason : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpHeadersRefreshRequiredReason(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The transport is making its first dynamic header request for this server. + public static McpHeadersRefreshRequiredReason Startup { get; } = new("startup"); + + /// The previously cached dynamic headers expired. + public static McpHeadersRefreshRequiredReason TtlExpired { get; } = new("ttl-expired"); + + /// The server returned 401 and stale dynamic headers were invalidated. + public static McpHeadersRefreshRequiredReason AuthFailed { get; } = new("auth-failed"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpHeadersRefreshRequiredReason left, McpHeadersRefreshRequiredReason right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpHeadersRefreshRequiredReason left, McpHeadersRefreshRequiredReason right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpHeadersRefreshRequiredReason other && Equals(other); + + /// + public bool Equals(McpHeadersRefreshRequiredReason other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override McpHeadersRefreshRequiredReason Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpHeadersRefreshRequiredReason value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpHeadersRefreshRequiredReason)); + } + } +} + +/// How the pending MCP headers refresh request resolved. +[JsonConverter(typeof(Converter))] +[DebuggerDisplay("{Value,nq}")] +public readonly struct McpHeadersRefreshCompletedOutcome : IEquatable +{ + private readonly string? _value; + + /// Initializes a new instance of the struct. + /// The value to associate with this . + [JsonConstructor] + public McpHeadersRefreshCompletedOutcome(string value) + { + ArgumentException.ThrowIfNullOrWhiteSpace(value); + _value = value; + } + + /// Gets the value associated with this . + public string Value => _value ?? string.Empty; + + /// The host supplied dynamic headers. + public static McpHeadersRefreshCompletedOutcome Headers { get; } = new("headers"); + + /// The host responded with no dynamic headers. + public static McpHeadersRefreshCompletedOutcome None { get; } = new("none"); + + /// No response arrived within the bounded window. + public static McpHeadersRefreshCompletedOutcome Timeout { get; } = new("timeout"); + + /// Returns a value indicating whether two instances are equivalent. + public static bool operator ==(McpHeadersRefreshCompletedOutcome left, McpHeadersRefreshCompletedOutcome right) => left.Equals(right); + + /// Returns a value indicating whether two instances are not equivalent. + public static bool operator !=(McpHeadersRefreshCompletedOutcome left, McpHeadersRefreshCompletedOutcome right) => !(left == right); + + /// + public override bool Equals(object? obj) => obj is McpHeadersRefreshCompletedOutcome other && Equals(other); + + /// + public bool Equals(McpHeadersRefreshCompletedOutcome other) => string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value); + + /// + public override string ToString() => Value; + + /// Provides a for serializing instances. + [EditorBrowsable(EditorBrowsableState.Never)] + public sealed class Converter : JsonConverter + { + /// + public override McpHeadersRefreshCompletedOutcome Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return new(GeneratedStringEnumJson.ReadValue(ref reader, typeToConvert)); + } + + /// + public override void Write(Utf8JsonWriter writer, McpHeadersRefreshCompletedOutcome value, JsonSerializerOptions options) + { + GeneratedStringEnumJson.WriteValue(writer, value.Value, typeof(McpHeadersRefreshCompletedOutcome)); + } + } +} + /// The user's auto-mode-switch choice. [JsonConverter(typeof(Converter))] [DebuggerDisplay("{Value,nq}")] @@ -9651,6 +10051,8 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] [JsonSerializable(typeof(AbortData))] [JsonSerializable(typeof(AbortEvent))] +[JsonSerializable(typeof(AssistantIdleData))] +[JsonSerializable(typeof(AssistantIdleEvent))] [JsonSerializable(typeof(AssistantIntentData))] [JsonSerializable(typeof(AssistantIntentEvent))] [JsonSerializable(typeof(AssistantMessageData))] @@ -9748,6 +10150,10 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(McpAppToolCallCompleteEvent))] [JsonSerializable(typeof(McpAppToolCallCompleteToolMeta))] [JsonSerializable(typeof(McpAppToolCallCompleteToolMetaUI))] +[JsonSerializable(typeof(McpHeadersRefreshCompletedData))] +[JsonSerializable(typeof(McpHeadersRefreshCompletedEvent))] +[JsonSerializable(typeof(McpHeadersRefreshRequiredData))] +[JsonSerializable(typeof(McpHeadersRefreshRequiredEvent))] [JsonSerializable(typeof(McpOauthCompletedData))] [JsonSerializable(typeof(McpOauthCompletedEvent))] [JsonSerializable(typeof(McpOauthRequiredData))] @@ -9803,6 +10209,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(PermissionRule))] [JsonSerializable(typeof(PersistedBinaryImage))] [JsonSerializable(typeof(PersistedBinaryResult))] +[JsonSerializable(typeof(ResponseBudgetConfig))] [JsonSerializable(typeof(SamplingCompletedData))] [JsonSerializable(typeof(SamplingCompletedEvent))] [JsonSerializable(typeof(SamplingRequestedData))] @@ -9956,6 +10363,7 @@ public override void Write(Utf8JsonWriter writer, ExtensionsLoadedExtensionStatu [JsonSerializable(typeof(ToolExecutionProgressEvent))] [JsonSerializable(typeof(ToolExecutionStartData))] [JsonSerializable(typeof(ToolExecutionStartEvent))] +[JsonSerializable(typeof(ToolExecutionStartShellToolInfo))] [JsonSerializable(typeof(ToolExecutionStartToolDescription))] [JsonSerializable(typeof(ToolExecutionStartToolDescriptionMeta))] [JsonSerializable(typeof(ToolExecutionStartToolDescriptionMetaUI))] diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index 0985848e26..7f883f4d5a 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -63,6 +63,7 @@ public sealed partial class CopilotSession : IAsyncDisposable private readonly CopilotClient _parentClient; private volatile Func>? _permissionHandler; + private volatile Func>? _mcpAuthHandler; private volatile Func>? _userInputHandler; private volatile Func>? _elicitationHandler; private volatile Func>? _exitPlanModeHandler; @@ -558,6 +559,11 @@ internal void RegisterPermissionHandler(Func>? handler) + { + _mcpAuthHandler = handler; + } + /// /// Handles a permission request from the Copilot CLI. /// @@ -633,6 +639,39 @@ private async Task HandleBroadcastEventAsync(SessionEvent sessionEvent) break; } + case McpOauthRequiredEvent authEvent: + { + var data = authEvent.Data; + if (string.IsNullOrEmpty(data.RequestId)) + return; + + var handler = _mcpAuthHandler; + if (handler is null) + { + if (_logger.IsEnabled(LogLevel.Warning)) + { + _logger.LogWarning( + "Received MCP OAuth request without a registered MCP auth handler. SessionId={SessionId}, RequestId={RequestId}", + SessionId, + data.RequestId); + } + return; + } + + await ExecuteMcpAuthAndRespondAsync(data.RequestId, new McpAuthContext + { + SessionId = SessionId, + RequestId = data.RequestId, + ServerName = data.ServerName, + ServerUrl = data.ServerUrl, + Reason = data.Reason, + WwwAuthenticateParams = data.WwwAuthenticateParams, + ResourceMetadata = data.ResourceMetadata, + StaticClientConfig = data.StaticClientConfig + }, handler); + break; + } + case CommandExecuteEvent cmdEvent: { var data = cmdEvent.Data; @@ -702,6 +741,80 @@ await HandleElicitationRequestAsync( } } + private async Task ExecuteMcpAuthAndRespondAsync( + string requestId, + McpAuthContext context, + Func> handler) + { + try + { + var result = await handler(context); + McpOauthPendingRequestResponse response = + result is { Cancelled: false, Token: { } token } + ? new McpOauthPendingRequestResponseToken + { + AccessToken = token.AccessToken, + TokenType = token.TokenType, + ExpiresIn = token.ExpiresIn + } + : new McpOauthPendingRequestResponseCancelled(); + + await Rpc.Mcp.Oauth.HandlePendingRequestAsync(requestId, response); + } + catch (OperationCanceledException) + { + await TryCancelMcpAuthRequestAsync(requestId); + } + catch (ObjectDisposedException) + { + await TryCancelMcpAuthRequestAsync(requestId); + } + catch (InvalidOperationException) + { + await TryCancelMcpAuthRequestAsync(requestId); + } + catch (ArgumentException) + { + await TryCancelMcpAuthRequestAsync(requestId); + } + catch (NotSupportedException) + { + await TryCancelMcpAuthRequestAsync(requestId); + } + catch (JsonException) + { + await TryCancelMcpAuthRequestAsync(requestId); + } + catch (RemoteRpcException) + { + await TryCancelMcpAuthRequestAsync(requestId); + } + catch (IOException) + { + await TryCancelMcpAuthRequestAsync(requestId); + } + } + + private async Task TryCancelMcpAuthRequestAsync(string requestId) + { + try + { + await Rpc.Mcp.Oauth.HandlePendingRequestAsync(requestId, new McpOauthPendingRequestResponseCancelled()); + } + catch (IOException) + { + // Connection lost — nothing we can do. + } + catch (ObjectDisposedException) + { + // Connection already disposed — nothing we can do. + } + catch (RemoteRpcException) + { + // The pending request may already be gone — nothing we can do. + } + } + /// /// Executes a tool handler and sends the result back via the HandlePendingToolCall RPC. /// diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 5ae9657813..ecb2774398 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -1128,6 +1128,72 @@ public sealed class ElicitationContext public string? Url { get; set; } } +/// +/// Context for an MCP OAuth request callback. +/// +[Experimental(Diagnostics.Experimental)] +public sealed class McpAuthContext +{ + /// Identifier of the session that triggered the MCP OAuth request. + public string SessionId { get; set; } = string.Empty; + + /// Identifier of the pending MCP OAuth request. + public string RequestId { get; set; } = string.Empty; + + /// Display name of the MCP server that requires OAuth. + public string ServerName { get; set; } = string.Empty; + + /// URL of the MCP server that requires OAuth. + public string ServerUrl { get; set; } = string.Empty; + + /// Why the runtime is requesting host-provided OAuth credentials. + public McpOauthRequestReason Reason { get; set; } + + /// Parsed WWW-Authenticate parameters from the MCP server, if available. + public McpOauthWWWAuthenticateParams? WwwAuthenticateParams { get; set; } + + /// Raw RFC 9728 protected-resource metadata JSON fetched by the runtime, if available. + public string? ResourceMetadata { get; set; } + + /// Static OAuth client configuration, if the server specifies one. + public McpOauthRequiredStaticClientConfig? StaticClientConfig { get; set; } +} + +/// +/// Host-provided OAuth token data for a pending MCP OAuth request. +/// +[Experimental(Diagnostics.Experimental)] +public sealed class McpAuthToken +{ + /// Access token acquired by the SDK host. + public required string AccessToken { get; set; } + + /// OAuth token type. Defaults to Bearer when omitted. + public string? TokenType { get; set; } + + /// Token lifetime in seconds, if known. + public long? ExpiresIn { get; set; } +} + +/// +/// Result returned by an MCP auth request handler. +/// +[Experimental(Diagnostics.Experimental)] +public sealed class McpAuthResult +{ + /// Whether the request should be cancelled instead of resolved with a token. + public bool Cancelled { get; set; } + + /// Host-provided token data. Ignored when is true. + public McpAuthToken? Token { get; set; } + + /// Create a token result. + public static McpAuthResult FromToken(McpAuthToken token) => new() { Token = token }; + + /// Create a cancellation result. + public static McpAuthResult Cancel() => new() { Cancelled = true }; +} + // ============================================================================ // Session Capabilities // ============================================================================ @@ -2719,6 +2785,7 @@ protected SessionConfigBase(SessionConfigBase? other) OnElicitationRequest = other.OnElicitationRequest; OnEvent = other.OnEvent; OnExitPlanModeRequest = other.OnExitPlanModeRequest; + OnMcpAuthRequest = other.OnMcpAuthRequest; OnPermissionRequest = other.OnPermissionRequest; OnUserInputRequest = other.OnUserInputRequest; Provider = other.Provider; @@ -3180,6 +3247,14 @@ protected SessionConfigBase(SessionConfigBase? other) [JsonIgnore] public ICanvasHandler? CanvasHandler { get; set; } #pragma warning restore GHCP001 + + /// + /// Optional handler for MCP OAuth requests from MCP servers. + /// When provided, the SDK can satisfy MCP server OAuth requests with host-provided token data or cancellation. + /// + [Experimental(Diagnostics.Experimental)] + [JsonIgnore] + public Func>? OnMcpAuthRequest { get; set; } } /// diff --git a/dotnet/test/E2E/McpOAuthE2ETests.cs b/dotnet/test/E2E/McpOAuthE2ETests.cs new file mode 100644 index 0000000000..cb553eb278 --- /dev/null +++ b/dotnet/test/E2E/McpOAuthE2ETests.cs @@ -0,0 +1,298 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.Rpc; +using GitHub.Copilot.Test.Harness; +using System.Diagnostics; +using System.Text.Json; +using Xunit; +using Xunit.Abstractions; + +namespace GitHub.Copilot.Test.E2E; + +public class McpOAuthE2ETests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "mcp_oauth", output) +{ + private const string ExpectedToken = "sdk-host-token"; + private const string RefreshToken = ExpectedToken + "-refresh"; + private const string UpscopeToken = ExpectedToken + "-upscope"; + private const string ReauthToken = ExpectedToken + "-reauth"; + + [Fact] + public async Task Should_Satisfy_MCP_OAuth_Using_Host_Provided_Token() + { + await using var oauthServer = await OAuthMcpServer.StartAsync(ExpectedToken); + var serverName = "oauth-protected-mcp"; + McpAuthContext? observedRequest = null; + + await using var session = await CreateSessionAsync(new SessionConfig + { + OnMcpAuthRequest = request => + { + observedRequest = request; + return Task.FromResult(McpAuthResult.FromToken(new McpAuthToken + { + AccessToken = ExpectedToken, + TokenType = "Bearer", + ExpiresIn = 3600 + })); + }, + McpServers = new Dictionary + { + [serverName] = new McpHttpServerConfig + { + Url = $"{oauthServer.Url}/mcp", + Tools = ["*"] + } + } + }); + + await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected); + var tools = await session.Rpc.Mcp.ListToolsAsync(serverName); + Assert.Contains(tools.Tools, tool => tool.Name == "whoami"); + + Assert.NotNull(observedRequest); + Assert.NotEmpty(observedRequest!.RequestId); + Assert.Equal(serverName, observedRequest!.ServerName); + Assert.Equal($"{oauthServer.Url}/mcp", observedRequest.ServerUrl); + Assert.Equal(McpOauthRequestReason.Initial, observedRequest.Reason); + Assert.NotNull(observedRequest.WwwAuthenticateParams); + Assert.Equal($"{oauthServer.Url}/.well-known/oauth-protected-resource", observedRequest.WwwAuthenticateParams!.ResourceMetadataUrl); + Assert.Equal("mcp.read", observedRequest.WwwAuthenticateParams.Scope); + Assert.Equal("invalid_token", observedRequest.WwwAuthenticateParams.Error); + + using var metadata = JsonDocument.Parse(observedRequest.ResourceMetadata!); + Assert.Equal($"{oauthServer.Url}/mcp", metadata.RootElement.GetProperty("resource").GetString()); + + var requests = await oauthServer.GetRequestsAsync(); + Assert.Contains(requests, request => request.Authorization is null); + Assert.Contains(requests, request => request.Authorization == $"Bearer {ExpectedToken}"); + } + + [Fact] + public async Task Should_Request_Replacement_Tokens_Across_MCP_OAuth_Lifecycle() + { + await using var oauthServer = await OAuthMcpServer.StartAsync(ExpectedToken); + var serverName = "oauth-lifecycle-mcp"; + List observedReasons = []; + var refreshCount = 0; + + await using var session = await CreateSessionAsync(new SessionConfig + { + EnableMcpApps = true, + OnMcpAuthRequest = request => + { + observedReasons.Add(request.Reason); + if (request.Reason == McpOauthRequestReason.Refresh) + { + refreshCount++; + Assert.NotNull(request.WwwAuthenticateParams); + Assert.Null(request.WwwAuthenticateParams!.ResourceMetadataUrl); + Assert.Equal("invalid_token", request.WwwAuthenticateParams.Error); + if (refreshCount > 1) + { + return Task.FromResult(McpAuthResult.Cancel()); + } + } + + if (request.Reason == McpOauthRequestReason.Upscope) + { + Assert.NotNull(request.WwwAuthenticateParams); + Assert.Equal($"{oauthServer.Url}/.well-known/oauth-protected-resource", request.WwwAuthenticateParams!.ResourceMetadataUrl); + Assert.Equal("mcp.write", request.WwwAuthenticateParams.Scope); + Assert.Equal("insufficient_scope", request.WwwAuthenticateParams.Error); + } + + var token = request.Reason == McpOauthRequestReason.Refresh + ? RefreshToken + : request.Reason == McpOauthRequestReason.Upscope + ? UpscopeToken + : request.Reason == McpOauthRequestReason.Reauth + ? ReauthToken + : ExpectedToken; + + return Task.FromResult(McpAuthResult.FromToken(new McpAuthToken + { + AccessToken = token + })); + }, + McpServers = new Dictionary + { + [serverName] = new McpHttpServerConfig + { + Url = $"{oauthServer.Url}/mcp", + Tools = ["*"] + } + } + }); + + await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Connected); + await CallWhoamiAsync(session, serverName, "refresh"); + await CallWhoamiAsync(session, serverName, "upscope"); + await CallWhoamiAsync(session, serverName, "reauth"); + + Assert.Equal( + [ + McpOauthRequestReason.Initial, + McpOauthRequestReason.Refresh, + McpOauthRequestReason.Upscope, + McpOauthRequestReason.Refresh, + McpOauthRequestReason.Reauth + ], + observedReasons); + + var requests = await oauthServer.GetRequestsAsync(); + Assert.Contains(requests, request => request.Authorization == $"Bearer {RefreshToken}"); + Assert.Contains(requests, request => request.Authorization == $"Bearer {UpscopeToken}"); + Assert.Contains(requests, request => request.Authorization == $"Bearer {ReauthToken}"); + } + + [Fact] + public async Task Should_Cancel_Pending_MCP_OAuth_Request() + { + await using var oauthServer = await OAuthMcpServer.StartAsync(ExpectedToken); + var serverName = "oauth-cancelled-mcp"; + McpAuthContext? observedRequest = null; + + await using var session = await CreateSessionAsync(new SessionConfig + { + OnMcpAuthRequest = request => + { + observedRequest = request; + return Task.FromResult(McpAuthResult.Cancel()); + }, + McpServers = new Dictionary + { + [serverName] = new McpHttpServerConfig + { + Url = $"{oauthServer.Url}/mcp", + Tools = ["*"] + } + } + }); + + await WaitForMcpServerStatusAsync(session, serverName, McpServerStatus.Failed); + + Assert.NotNull(observedRequest); + Assert.NotEmpty(observedRequest!.RequestId); + Assert.Equal(serverName, observedRequest!.ServerName); + Assert.Equal(McpOauthRequestReason.Initial, observedRequest.Reason); + } + + private static async Task CallWhoamiAsync(CopilotSession session, string serverName, string scenario) + { + using var argumentDocument = JsonDocument.Parse($"{{\"scenario\":\"{scenario}\"}}"); + var result = await session.Rpc.Mcp.Apps.CallToolAsync( + serverName, + "whoami", + serverName, + new Dictionary + { + ["scenario"] = argumentDocument.RootElement.GetProperty("scenario").Clone() + }); + + var content = result["content"].EnumerateArray().ToList(); + Assert.Single(content); + Assert.Equal("oauth-test-user", content[0].GetProperty("text").GetString()); + } + + private sealed class OAuthMcpServer : IAsyncDisposable + { + private readonly Process _process; + private readonly HttpClient _http = new(); + + private OAuthMcpServer(Process process, string url) + { + _process = process; + Url = url; + } + + public string Url { get; } + + public static async Task StartAsync(string expectedToken) + { + var repoRoot = FindRepoRoot(); + var script = GetRepoRelativePath(repoRoot, "test", "harness", "test-mcp-oauth-server.mjs"); + var startInfo = new ProcessStartInfo + { + FileName = "node", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + startInfo.ArgumentList.Add(script); + startInfo.Environment["EXPECTED_TOKEN"] = expectedToken; + + var process = Process.Start(startInfo) + ?? throw new InvalidOperationException("Failed to start OAuth MCP server."); + var stderrTask = process.StandardError.ReadToEndAsync(); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + while (!cts.IsCancellationRequested) + { + var line = await process.StandardOutput.ReadLineAsync(cts.Token); + if (line is null) + { + throw new InvalidOperationException($"OAuth MCP server exited before listening: {await stderrTask}"); + } + if (line.StartsWith("Listening: ", StringComparison.Ordinal)) + { + return new OAuthMcpServer(process, line["Listening: ".Length..]); + } + } + + throw new TimeoutException($"Timed out waiting for OAuth MCP server: {await stderrTask}"); + } + + public async Task> GetRequestsAsync() + { + var json = await _http.GetStringAsync($"{Url}/__requests"); + using var document = JsonDocument.Parse(json); + return document.RootElement.EnumerateArray() + .Select(element => new OAuthMcpRequest( + element.TryGetProperty("authorization", out var authorization) + && authorization.ValueKind is JsonValueKind.String + ? authorization.GetString() + : null)) + .ToList(); + } + + public async ValueTask DisposeAsync() + { + _http.Dispose(); + if (!_process.HasExited) + { + _process.Kill(entireProcessTree: true); + await _process.WaitForExitAsync(); + } + _process.Dispose(); + } + + private static string FindRepoRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir != null) + { + var candidate = GetRepoRelativePath(dir.FullName, "test", "harness", "test-mcp-oauth-server.mjs"); + if (File.Exists(candidate)) + return dir.FullName; + dir = dir.Parent; + } + throw new InvalidOperationException("Could not find repository root."); + } + + private static string GetRepoRelativePath(string repoRoot, params string[] relativeSegments) + { + var path = repoRoot; + foreach (var segment in relativeSegments) + { + if (Path.IsPathRooted(segment)) + throw new ArgumentException("Repository-relative path segments must not be rooted.", nameof(relativeSegments)); + path = Path.Join(path, segment); + } + return Path.GetFullPath(path); + } + } + + private sealed record OAuthMcpRequest(string? Authorization); +} diff --git a/dotnet/test/Harness/E2ETestContext.cs b/dotnet/test/Harness/E2ETestContext.cs index 2e2043183a..464325e6fa 100644 --- a/dotnet/test/Harness/E2ETestContext.cs +++ b/dotnet/test/Harness/E2ETestContext.cs @@ -140,6 +140,9 @@ private static string GetCliPath(string repoRoot) var envPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"); if (!string.IsNullOrEmpty(envPath)) return envPath; + const string localRuntimeCliPath = "/Users/roji/.copilot/repos/copilot-worktrees/copilot-agent-runtime/roji-symmetrical-dollop/dist-cli/index.js"; + if (File.Exists(localRuntimeCliPath)) return localRuntimeCliPath; + // As of CLI 1.0.64-1 the @github/copilot package is a thin loader; the // runnable index.js ships in the installed platform package // (e.g. @github/copilot-linux-x64). Exactly one is installed. @@ -192,6 +195,8 @@ public Dictionary GetEnvironment() env["GH_CONFIG_DIR"] = HomeDir; env["XDG_CONFIG_HOME"] = HomeDir; env["XDG_STATE_HOME"] = HomeDir; + env["COPILOT_MCP_APPS"] = "true"; + env["MCP_APPS"] = "true"; if (!string.IsNullOrEmpty(_proxy.ConnectProxyUrl) && !string.IsNullOrEmpty(_proxy.CaFilePath)) { const string noProxy = "127.0.0.1,localhost,::1"; diff --git a/dotnet/test/Unit/ClientSessionLifetimeTests.cs b/dotnet/test/Unit/ClientSessionLifetimeTests.cs index 2c11c7d6b5..3864c8b8c8 100644 --- a/dotnet/test/Unit/ClientSessionLifetimeTests.cs +++ b/dotnet/test/Unit/ClientSessionLifetimeTests.cs @@ -16,6 +16,8 @@ namespace GitHub.Copilot.Test.Unit; public sealed class ClientSessionLifetimeTests { + private sealed record RpcRequestRecord(string Method, JsonElement Params); + [Fact] public async Task StopAsync_Requests_Runtime_Shutdown_For_Owned_Process() { @@ -188,6 +190,124 @@ public async Task ResumeSessionAsync_Throws_When_Same_Client_Already_Tracks_Sess AssertSessionCount(client, sessions: 1); } + [Fact] + public async Task CreateSessionAsync_Registers_McpAuth_Interest_Only_When_Handler_Configured() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + + await using var withoutAuth = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + OnEvent = _ => { } + }); + + Assert.DoesNotContain(server.Requests, request => + request.Method == "session.eventLog.registerInterest" + && request.Params.GetProperty("eventType").GetString() == "mcp.oauth_required"); + Assert.Contains(server.Requests, request => + request.Method == "session.create" + && request.Params.GetProperty("requestPermission").GetBoolean()); + + server.ClearRequests(); + + await using var withAuth = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + OnMcpAuthRequest = _ => Task.FromResult(McpAuthResult.Cancel()) + }); + + Assert.Collection( + server.Requests.Take(2), + request => Assert.Equal("session.create", request.Method), + request => + { + Assert.Equal("session.eventLog.registerInterest", request.Method); + Assert.Equal("mcp.oauth_required", request.Params.GetProperty("eventType").GetString()); + }); + } + + [Fact] + public async Task CreateSessionAsync_Registers_McpAuth_Interest_After_Cloud_Create_When_Handler_Configured() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + var cloud = new CloudSessionOptions + { + Repository = new CloudSessionRepository + { + Owner = "github", + Name = "copilot-sdk", + Branch = "main" + } + }; + + await using var withoutAuth = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + Cloud = cloud + }); + + Assert.DoesNotContain(server.Requests, request => + request.Method == "session.eventLog.registerInterest" + && request.Params.GetProperty("eventType").GetString() == "mcp.oauth_required"); + + server.ClearRequests(); + + await using var withAuth = await client.CreateSessionAsync(new SessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + OnMcpAuthRequest = _ => Task.FromResult(McpAuthResult.Cancel()), + Cloud = cloud + }); + + Assert.Collection( + server.Requests.Take(2), + request => Assert.Equal("session.create", request.Method), + request => + { + Assert.Equal("session.eventLog.registerInterest", request.Method); + Assert.Equal("mcp.oauth_required", request.Params.GetProperty("eventType").GetString()); + }); + } + + [Fact] + public async Task ResumeSessionAsync_Registers_McpAuth_Interest_Only_When_Handler_Configured() + { + await using var server = await FakeCopilotServer.StartAsync(); + await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) }); + + await using var withoutAuth = await client.ResumeSessionAsync("session-without-auth", new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + OnEvent = _ => { } + }); + + Assert.DoesNotContain(server.Requests, request => + request.Method == "session.eventLog.registerInterest" + && request.Params.GetProperty("eventType").GetString() == "mcp.oauth_required"); + Assert.Contains(server.Requests, request => + request.Method == "session.resume" + && request.Params.GetProperty("requestPermission").GetBoolean()); + + server.ClearRequests(); + + await using var withAuth = await client.ResumeSessionAsync("session-with-auth", new ResumeSessionConfig + { + OnPermissionRequest = PermissionHandler.ApproveAll, + OnMcpAuthRequest = _ => Task.FromResult(McpAuthResult.Cancel()) + }); + + Assert.Collection( + server.Requests.Take(2), + request => + { + Assert.Equal("session.eventLog.registerInterest", request.Method); + Assert.Equal("mcp.oauth_required", request.Params.GetProperty("eventType").GetString()); + }, + request => Assert.Equal("session.resume", request.Method)); + } + [Fact] public async Task Generated_Session_Rpc_Throws_When_Session_Disposed() { @@ -277,6 +397,8 @@ private sealed class FakeCopilotServer : IAsyncDisposable private readonly TaskCompletionSource _destroyStarted = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly TaskCompletionSource _allowDestroy = new(TaskCreationOptions.RunContinuationsAsynchronously); private readonly Task _serverTask; + private readonly List _requests = []; + private readonly object _requestsLock = new(); private string? _lastSessionId; private bool _delayDestroy; private bool _failRuntimeShutdown; @@ -307,6 +429,25 @@ public static Task StartAsync() public int RuntimeShutdownCount { get; private set; } + public IReadOnlyList Requests + { + get + { + lock (_requestsLock) + { + return _requests.ToArray(); + } + } + } + + public void ClearRequests() + { + lock (_requestsLock) + { + _requests.Clear(); + } + } + public void DelayDestroy() { _delayDestroy = true; @@ -382,6 +523,13 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel return; } + var paramsElement = request.TryGetProperty("params", out var rawParams) + ? rawParams.Clone() + : JsonDocument.Parse("{}").RootElement.Clone(); + lock (_requestsLock) + { + _requests.Add(new RpcRequestRecord(method!, paramsElement)); + } object? result = method switch { "connect" => new Dictionary @@ -392,6 +540,10 @@ private async Task HandleRequestAsync(Stream stream, JsonElement request, Cancel }, "session.create" => CreateSessionResult(request), "session.resume" => CreateSessionResult(request), + "session.eventLog.registerInterest" => new Dictionary + { + ["id"] = "interest-1" + }, "session.send" => new Dictionary { ["messageId"] = "message-1" diff --git a/dotnet/test/Unit/PublicDtoTests.cs b/dotnet/test/Unit/PublicDtoTests.cs index c81a8a7a64..d1918d2b9a 100644 --- a/dotnet/test/Unit/PublicDtoTests.cs +++ b/dotnet/test/Unit/PublicDtoTests.cs @@ -20,6 +20,25 @@ namespace GitHub.Copilot.Test.Unit; /// public class PublicDtoTests { + [Fact] + public void McpAuth_Result_Factories_Represent_Token_And_Cancellation() + { + var token = new McpAuthToken + { + AccessToken = "host-token", + TokenType = "Bearer", + ExpiresIn = 3600, + }; + + var tokenResult = McpAuthResult.FromToken(token); + Assert.Same(token, tokenResult.Token); + Assert.False(tokenResult.Cancelled); + + var cancelled = McpAuthResult.Cancel(); + Assert.True(cancelled.Cancelled); + Assert.Null(cancelled.Token); + } + [Fact] public void Public_Dto_Properties_Can_Be_Set_And_Read() { diff --git a/dotnet/test/Unit/SessionEventSerializationTests.cs b/dotnet/test/Unit/SessionEventSerializationTests.cs index 47b4ac3f73..64e28a5aee 100644 --- a/dotnet/test/Unit/SessionEventSerializationTests.cs +++ b/dotnet/test/Unit/SessionEventSerializationTests.cs @@ -150,14 +150,21 @@ public class SessionEventSerializationTests Data = new McpOauthRequiredData { RequestId = "oauth-request", + Reason = McpOauthRequestReason.Initial, ServerName = "oauth-server", ServerUrl = "https://example.com/mcp", StaticClientConfig = new McpOauthRequiredStaticClientConfig { ClientId = "client-id", + ClientSecret = "static-secret", GrantType = "client_credentials", PublicClient = false, }, + WwwAuthenticateParams = new McpOauthWWWAuthenticateParams + { + ResourceMetadataUrl = "https://example.com/.well-known/oauth-protected-resource", + }, + ResourceMetadata = """{"resource":"https://example.com/mcp"}""", }, }, "mcp.oauth_required" @@ -281,6 +288,17 @@ public void SessionEvent_ToJson_RoundTrips_JsonElementBackedPayloads(SessionEven .GetProperty("staticClientConfig") .GetProperty("grantType") .GetString()); + Assert.Equal( + "static-secret", + root.GetProperty("data") + .GetProperty("staticClientConfig") + .GetProperty("clientSecret") + .GetString()); + Assert.Equal( + """{"resource":"https://example.com/mcp"}""", + root.GetProperty("data") + .GetProperty("resourceMetadata") + .GetString()); break; case "assistant.message_start": @@ -297,4 +315,57 @@ public void SessionEvent_ToJson_RoundTrips_JsonElementBackedPayloads(SessionEven break; } } + + [Fact] + public void McpOauthRequiredData_Allows_Missing_Optional_Metadata() + { + const string json = """ + { + "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "timestamp": "2026-03-15T21:26:54.987Z", + "parentId": null, + "type": "mcp.oauth_required", + "data": { + "requestId": "oauth-request", + "reason": "initial", + "serverName": "oauth-server", + "serverUrl": "https://example.com/mcp" + } + } + """; + + var authEvent = Assert.IsType(SessionEvent.FromJson(json)); + Assert.Null(authEvent.Data.WwwAuthenticateParams); + Assert.Null(authEvent.Data.ResourceMetadata); + } + + [Fact] + public void McpOauthRequiredData_Preserves_Static_Client_Secret() + { + const string json = """ + { + "id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb", + "timestamp": "2026-03-15T21:26:54.987Z", + "parentId": null, + "type": "mcp.oauth_required", + "data": { + "requestId": "oauth-request", + "reason": "initial", + "serverName": "oauth-server", + "serverUrl": "https://example.com/mcp", + "staticClientConfig": { + "clientId": "static-client", + "clientSecret": "static-secret", + "grantType": "client_credentials", + "publicClient": false + } + } + } + """; + + var authEvent = Assert.IsType(SessionEvent.FromJson(json)); + + Assert.NotNull(authEvent.Data.StaticClientConfig); + Assert.Equal("static-secret", authEvent.Data.StaticClientConfig.ClientSecret); + } } diff --git a/go/client.go b/go/client.go index 970f046425..9e2819047e 100644 --- a/go/client.go +++ b/go/client.go @@ -806,6 +806,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses s.registerTools(config.Tools) s.registerPermissionHandler(config.OnPermissionRequest) + s.registerMCPAuthHandler(config.OnMCPAuthRequest) if config.OnUserInputRequest != nil { s.registerUserInputHandler(config.OnUserInputRequest) } @@ -937,6 +938,14 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses c.sessionsMux.Unlock() return nil, fmt.Errorf("session.create returned sessionId %s but the caller requested %s", response.SessionID, localSessionID) } + if config.OnMCPAuthRequest != nil { + if _, err := c.client.Request(ctx, "session.eventLog.registerInterest", map[string]any{ + "sessionId": session.SessionID, + "eventType": "mcp.oauth_required", + }); err != nil { + return nil, err + } + } session.workspacePath = response.WorkspacePath session.setCapabilities(response.Capabilities) @@ -1106,6 +1115,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, session.registerTools(config.Tools) session.registerPermissionHandler(config.OnPermissionRequest) + session.registerMCPAuthHandler(config.OnMCPAuthRequest) if config.OnUserInputRequest != nil { session.registerUserInputHandler(config.OnUserInputRequest) } @@ -1140,6 +1150,17 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string, c.sessionsMux.Lock() c.sessions[sessionID] = session c.sessionsMux.Unlock() + if config.OnMCPAuthRequest != nil { + if _, err := c.client.Request(ctx, "session.eventLog.registerInterest", map[string]any{ + "sessionId": sessionID, + "eventType": "mcp.oauth_required", + }); err != nil { + c.sessionsMux.Lock() + delete(c.sessions, sessionID) + c.sessionsMux.Unlock() + return nil, err + } + } if c.options.SessionFS != nil { if config.CreateSessionFSProvider == nil { diff --git a/go/client_test.go b/go/client_test.go index d59c71c6f9..dce543ea6b 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -3,6 +3,8 @@ package copilot import ( "context" "encoding/json" + "fmt" + "io" "net" "os" "os/exec" @@ -1315,6 +1317,291 @@ func TestClient_StartStopRace(t *testing.T) { } } +func TestClient_MCPAuthInterestRegistration(t *testing.T) { + t.Run("create skips MCP OAuth interest without auth handler", func(t *testing.T) { + client, requests, cleanup := newInMemoryClient(t) + defer cleanup() + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + OnEvent: func(SessionEvent) {}, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + defer session.Disconnect() + + assertNoMCPAuthInterest(t, requests.snapshot()) + assertRequestMethod(t, requests.snapshot(), "session.create") + assertCreateRequestPermission(t, requests.snapshot()) + }) + + t.Run("create registers MCP OAuth interest after local session create when auth handler is configured", func(t *testing.T) { + client, requests, cleanup := newInMemoryClient(t) + defer cleanup() + + session, err := client.CreateSession(t.Context(), &SessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + OnMCPAuthRequest: func(MCPAuthRequest, MCPAuthInvocation) (*MCPAuthResult, error) { + return &MCPAuthResult{Kind: "cancelled"}, nil + }, + }) + if err != nil { + t.Fatalf("CreateSession failed: %v", err) + } + defer session.Disconnect() + + snapshot := requests.snapshot() + assertRequestMethod(t, snapshot, "session.eventLog.registerInterest") + if snapshot[0].Method != "session.create" { + t.Fatalf("expected session.create before MCP auth interest, got %s", snapshot[0].Method) + } + if snapshot[1].Method != "session.eventLog.registerInterest" { + t.Fatalf("expected MCP auth interest after session.create, got %s", snapshot[1].Method) + } + assertMCPAuthInterest(t, snapshot[1]) + assertCreateRequestPermission(t, snapshot) + }) + + t.Run("cloud create registers MCP OAuth interest after server assigns id only when auth handler is configured", func(t *testing.T) { + client, requests, cleanup := newInMemoryClient(t) + defer cleanup() + + withoutAuth, err := client.CreateSession(t.Context(), &SessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + Cloud: &CloudSessionOptions{ + Repository: &CloudSessionRepository{Owner: "github", Name: "copilot-sdk", Branch: "main"}, + }, + }) + if err != nil { + t.Fatalf("CreateSession without auth failed: %v", err) + } + defer withoutAuth.Disconnect() + + assertNoMCPAuthInterest(t, requests.snapshot()) + requests.clear() + + withAuth, err := client.CreateSession(t.Context(), &SessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + OnMCPAuthRequest: func(MCPAuthRequest, MCPAuthInvocation) (*MCPAuthResult, error) { + return &MCPAuthResult{Kind: "cancelled"}, nil + }, + Cloud: &CloudSessionOptions{ + Repository: &CloudSessionRepository{Owner: "github", Name: "copilot-sdk", Branch: "main"}, + }, + }) + if err != nil { + t.Fatalf("CreateSession with auth failed: %v", err) + } + defer withAuth.Disconnect() + + snapshot := requests.snapshot() + if snapshot[0].Method != "session.create" { + t.Fatalf("expected cloud session.create before MCP auth interest, got %s", snapshot[0].Method) + } + if snapshot[1].Method != "session.eventLog.registerInterest" { + t.Fatalf("expected MCP auth interest after cloud session.create, got %s", snapshot[1].Method) + } + assertMCPAuthInterest(t, snapshot[1]) + }) + + t.Run("resume conditionally registers MCP OAuth interest before session resume", func(t *testing.T) { + client, requests, cleanup := newInMemoryClient(t) + defer cleanup() + + withoutAuth, err := client.ResumeSession(t.Context(), "session-without-auth", &ResumeSessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + OnEvent: func(SessionEvent) {}, + }) + if err != nil { + t.Fatalf("ResumeSession without auth failed: %v", err) + } + defer withoutAuth.Disconnect() + + assertNoMCPAuthInterest(t, requests.snapshot()) + assertRequestMethod(t, requests.snapshot(), "session.resume") + requests.clear() + + withAuth, err := client.ResumeSession(t.Context(), "session-with-auth", &ResumeSessionConfig{ + OnPermissionRequest: PermissionHandler.ApproveAll, + OnMCPAuthRequest: func(MCPAuthRequest, MCPAuthInvocation) (*MCPAuthResult, error) { + return &MCPAuthResult{Kind: "cancelled"}, nil + }, + }) + if err != nil { + t.Fatalf("ResumeSession with auth failed: %v", err) + } + defer withAuth.Disconnect() + + snapshot := requests.snapshot() + if snapshot[0].Method != "session.eventLog.registerInterest" { + t.Fatalf("expected MCP auth interest before session.resume, got %s", snapshot[0].Method) + } + if snapshot[1].Method != "session.resume" { + t.Fatalf("expected session.resume after MCP auth interest, got %s", snapshot[1].Method) + } + assertMCPAuthInterest(t, snapshot[0]) + }) +} + +type recordedRequest struct { + Method string + Params map[string]any +} + +type requestRecorder struct { + mu sync.Mutex + requests []recordedRequest +} + +func (r *requestRecorder) append(request recordedRequest) { + r.mu.Lock() + defer r.mu.Unlock() + r.requests = append(r.requests, request) +} + +func (r *requestRecorder) snapshot() []recordedRequest { + r.mu.Lock() + defer r.mu.Unlock() + out := make([]recordedRequest, len(r.requests)) + copy(out, r.requests) + return out +} + +func (r *requestRecorder) clear() { + r.mu.Lock() + defer r.mu.Unlock() + r.requests = nil +} + +func newInMemoryClient(t *testing.T) (*Client, *requestRecorder, func()) { + t.Helper() + + stdinR, stdinW := io.Pipe() + stdoutR, stdoutW := io.Pipe() + rpcClient := jsonrpc2.NewClient(stdinW, stdoutR) + rpcClient.Start() + + client := NewClient(&ClientOptions{}) + client.client = rpcClient + client.RPC = rpc.NewServerRPC(rpcClient) + client.state = stateConnected + + requests := &requestRecorder{} + done := make(chan struct{}) + go serveInMemoryRuntime(t, stdinR, stdoutW, requests, done) + + cleanup := func() { + rpcClient.Stop() + stdinR.Close() + stdinW.Close() + stdoutR.Close() + stdoutW.Close() + <-done + } + return client, requests, cleanup +} + +func serveInMemoryRuntime(t *testing.T, stdinR *io.PipeReader, stdoutW *io.PipeWriter, requests *requestRecorder, done chan<- struct{}) { + t.Helper() + defer close(done) + + serverAssignedSessions := 0 + for { + frame, err := readTestJSONRPCFrame(stdinR) + if err != nil { + return + } + + var request struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` + } + if err := json.Unmarshal(frame, &request); err != nil { + t.Errorf("failed to unmarshal JSON-RPC request: %v", err) + return + } + requests.append(recordedRequest{Method: request.Method, Params: request.Params}) + + result := map[string]any{} + switch request.Method { + case "session.create", "session.resume": + sessionID, _ := request.Params["sessionId"].(string) + if sessionID == "" { + serverAssignedSessions++ + sessionID = fmt.Sprintf("server-assigned-session-%d", serverAssignedSessions) + } + result = map[string]any{"sessionId": sessionID, "workspacePath": nil} + case "session.eventLog.registerInterest": + result = map[string]any{"id": "interest-1"} + case "session.options.update": + result = map[string]any{"success": true} + case "session.skills.reload", "session.destroy": + result = map[string]any{} + default: + t.Errorf("unexpected JSON-RPC method %s", request.Method) + return + } + + response := map[string]any{ + "jsonrpc": "2.0", + "id": json.RawMessage(request.ID), + "result": result, + } + data, err := json.Marshal(response) + if err != nil { + t.Errorf("failed to marshal JSON-RPC response: %v", err) + return + } + if _, err := fmt.Fprintf(stdoutW, "Content-Length: %d\r\n\r\n%s", len(data), data); err != nil { + return + } + } +} + +func assertRequestMethod(t *testing.T, requests []recordedRequest, method string) { + t.Helper() + for _, request := range requests { + if request.Method == method { + return + } + } + t.Fatalf("expected %s request in %+v", method, requests) +} + +func assertNoMCPAuthInterest(t *testing.T, requests []recordedRequest) { + t.Helper() + for _, request := range requests { + if request.Method == "session.eventLog.registerInterest" && request.Params["eventType"] == "mcp.oauth_required" { + t.Fatalf("did not expect MCP auth interest registration in %+v", requests) + } + } +} + +func assertMCPAuthInterest(t *testing.T, request recordedRequest) { + t.Helper() + if request.Method != "session.eventLog.registerInterest" { + t.Fatalf("expected registerInterest request, got %s", request.Method) + } + if request.Params["eventType"] != "mcp.oauth_required" { + t.Fatalf("expected mcp.oauth_required interest, got %v", request.Params["eventType"]) + } +} + +func assertCreateRequestPermission(t *testing.T, requests []recordedRequest) { + t.Helper() + for _, request := range requests { + if request.Method == "session.create" { + if request.Params["requestPermission"] != true { + t.Fatalf("expected create requestPermission=true, got %v", request.Params["requestPermission"]) + } + return + } + } + t.Fatalf("session.create request not found in %+v", requests) +} + func TestCreateSessionRequest_Commands(t *testing.T) { t.Run("forwards commands in session.create RPC", func(t *testing.T) { req := createSessionRequest{ diff --git a/go/internal/e2e/mcp_oauth_e2e_test.go b/go/internal/e2e/mcp_oauth_e2e_test.go new file mode 100644 index 0000000000..ceebe0245d --- /dev/null +++ b/go/internal/e2e/mcp_oauth_e2e_test.go @@ -0,0 +1,328 @@ +package e2e + +import ( + "bufio" + "encoding/json" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" + "testing" + "time" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" +) + +const expectedMCPOAuthToken = "sdk-host-token" +const refreshMCPOAuthToken = expectedMCPOAuthToken + "-refresh" +const upscopeMCPOAuthToken = expectedMCPOAuthToken + "-upscope" +const reauthMCPOAuthToken = expectedMCPOAuthToken + "-reauth" + +func TestMCPOAuthE2E(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + t.Run("satisfy MCP OAuth using host-provided token", func(t *testing.T) { + baseURL := startOAuthMCPServer(t) + serverName := "oauth-protected-mcp" + tokenType := "Bearer" + expiresIn := int64(3600) + var observedRequest copilot.MCPAuthRequest + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + OnMCPAuthRequest: func(request copilot.MCPAuthRequest, _ copilot.MCPAuthInvocation) (*copilot.MCPAuthResult, error) { + observedRequest = request + return &copilot.MCPAuthResult{ + Kind: "token", + Token: &copilot.MCPAuthToken{ + AccessToken: expectedMCPOAuthToken, + TokenType: &tokenType, + ExpiresIn: &expiresIn, + }, + }, nil + }, + MCPServers: map[string]copilot.MCPServerConfig{ + serverName: copilot.MCPHTTPServerConfig{ + URL: baseURL + "/mcp", + Tools: []string{"*"}, + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + t.Cleanup(func() { session.Disconnect() }) + + waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) + tools, err := session.RPC.MCP.ListTools(t.Context(), &rpc.MCPListToolsRequest{ServerName: serverName}) + if err != nil { + t.Fatalf("Failed to list MCP tools: %v", err) + } + if len(tools.Tools) != 1 || tools.Tools[0].Name != "whoami" { + t.Fatalf("Expected whoami tool, got %#v", tools.Tools) + } + + if observedRequest.ServerName != serverName { + t.Fatalf("Expected serverName %q, got %q", serverName, observedRequest.ServerName) + } + if observedRequest.ServerURL != baseURL+"/mcp" { + t.Fatalf("Expected serverUrl %q, got %q", baseURL+"/mcp", observedRequest.ServerURL) + } + if observedRequest.WwwAuthenticateParams == nil { + t.Fatal("Expected WWW-Authenticate params") + } + if observedRequest.Reason != "initial" { + t.Fatalf("Unexpected auth request reason: %q", observedRequest.Reason) + } + if observedRequest.WwwAuthenticateParams.ResourceMetadataURL == nil || + *observedRequest.WwwAuthenticateParams.ResourceMetadataURL != baseURL+"/.well-known/oauth-protected-resource" { + t.Fatalf("Unexpected resource metadata URL: %v", observedRequest.WwwAuthenticateParams.ResourceMetadataURL) + } + if observedRequest.WwwAuthenticateParams.Scope != "mcp.read" || observedRequest.WwwAuthenticateParams.Error != "invalid_token" { + t.Fatalf("Unexpected WWW-Authenticate params: %#v", observedRequest.WwwAuthenticateParams) + } + + var metadata map[string]any + if err := json.Unmarshal([]byte(observedRequest.ResourceMetadata), &metadata); err != nil { + t.Fatalf("Failed to parse resource metadata: %v", err) + } + if metadata["resource"] != baseURL+"/mcp" { + t.Fatalf("Expected resource %q, got %#v", baseURL+"/mcp", metadata["resource"]) + } + + requests := fetchOAuthMCPRequests(t, baseURL) + if !hasAuthorization(requests, "") { + t.Fatal("Expected at least one unauthenticated MCP request") + } + if !hasAuthorization(requests, "Bearer "+expectedMCPOAuthToken) { + t.Fatal("Expected at least one MCP request with host-provided token") + } + }) + + t.Run("request replacement tokens across MCP OAuth lifecycle", func(t *testing.T) { + baseURL := startOAuthMCPServer(t) + serverName := "oauth-lifecycle-mcp" + var mu sync.Mutex + var observedReasons []string + refreshCount := 0 + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + EnableMCPApps: true, + OnMCPAuthRequest: func(request copilot.MCPAuthRequest, _ copilot.MCPAuthInvocation) (*copilot.MCPAuthResult, error) { + mu.Lock() + observedReasons = append(observedReasons, request.Reason) + refreshOrdinal := 0 + if request.Reason == "refresh" { + refreshCount++ + refreshOrdinal = refreshCount + } + mu.Unlock() + + token := expectedMCPOAuthToken + switch request.Reason { + case "refresh": + if request.WwwAuthenticateParams == nil || + request.WwwAuthenticateParams.ResourceMetadataURL != nil || + request.WwwAuthenticateParams.Error != "invalid_token" { + t.Fatalf("Unexpected refresh WWW-Authenticate params: %#v", request.WwwAuthenticateParams) + } + if refreshOrdinal > 1 { + return &copilot.MCPAuthResult{Kind: "cancelled"}, nil + } + token = refreshMCPOAuthToken + case "upscope": + token = upscopeMCPOAuthToken + if request.WwwAuthenticateParams == nil || + request.WwwAuthenticateParams.ResourceMetadataURL == nil || + *request.WwwAuthenticateParams.ResourceMetadataURL != baseURL+"/.well-known/oauth-protected-resource" || + request.WwwAuthenticateParams.Scope != "mcp.write" || + request.WwwAuthenticateParams.Error != "insufficient_scope" { + t.Fatalf("Unexpected upscope WWW-Authenticate params: %#v", request.WwwAuthenticateParams) + } + case "reauth": + token = reauthMCPOAuthToken + } + return &copilot.MCPAuthResult{ + Kind: "token", + Token: &copilot.MCPAuthToken{AccessToken: token}, + }, nil + }, + MCPServers: map[string]copilot.MCPServerConfig{ + serverName: copilot.MCPHTTPServerConfig{ + URL: baseURL + "/mcp", + Tools: []string{"*"}, + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + t.Cleanup(func() { session.Disconnect() }) + + waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusConnected) + callWhoami(t, session, serverName, "refresh") + callWhoami(t, session, serverName, "upscope") + callWhoami(t, session, serverName, "reauth") + + mu.Lock() + reasons := append([]string(nil), observedReasons...) + mu.Unlock() + if strings.Join(reasons, ",") != "initial,refresh,upscope,refresh,reauth" { + t.Fatalf("Unexpected auth request reasons: %#v", reasons) + } + + requests := fetchOAuthMCPRequests(t, baseURL) + if !hasAuthorization(requests, "Bearer "+refreshMCPOAuthToken) { + t.Fatal("Expected at least one MCP request with refresh token") + } + if !hasAuthorization(requests, "Bearer "+upscopeMCPOAuthToken) { + t.Fatal("Expected at least one MCP request with upscope token") + } + if !hasAuthorization(requests, "Bearer "+reauthMCPOAuthToken) { + t.Fatal("Expected at least one MCP request with reauth token") + } + }) + + t.Run("cancel pending MCP OAuth request", func(t *testing.T) { + baseURL := startOAuthMCPServer(t) + serverName := "oauth-cancelled-mcp" + var observedRequest copilot.MCPAuthRequest + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + OnMCPAuthRequest: func(request copilot.MCPAuthRequest, _ copilot.MCPAuthInvocation) (*copilot.MCPAuthResult, error) { + observedRequest = request + return &copilot.MCPAuthResult{Kind: "cancelled"}, nil + }, + MCPServers: map[string]copilot.MCPServerConfig{ + serverName: copilot.MCPHTTPServerConfig{ + URL: baseURL + "/mcp", + Tools: []string{"*"}, + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + t.Cleanup(func() { session.Disconnect() }) + + waitForMCPServerStatus(t, session, serverName, rpc.MCPServerStatusFailed) + if observedRequest.ServerName != serverName { + t.Fatalf("Expected serverName %q, got %q", serverName, observedRequest.ServerName) + } + if observedRequest.Reason != "initial" { + t.Fatalf("Unexpected auth request reason: %q", observedRequest.Reason) + } + }) +} + +type oauthMCPRequest struct { + Authorization *string `json:"authorization"` +} + +func startOAuthMCPServer(t *testing.T) string { + t.Helper() + + serverPath, err := filepath.Abs("../../../test/harness/test-mcp-oauth-server.mjs") + if err != nil { + t.Fatalf("Failed to resolve OAuth MCP server path: %v", err) + } + cmd := exec.Command("node", serverPath) + cmd.Env = append(os.Environ(), "EXPECTED_TOKEN="+expectedMCPOAuthToken) + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatalf("Failed to pipe OAuth MCP server stdout: %v", err) + } + var stderr strings.Builder + cmd.Stderr = &stderr + if err := cmd.Start(); err != nil { + t.Fatalf("Failed to start OAuth MCP server: %v", err) + } + t.Cleanup(func() { + if cmd.ProcessState != nil && cmd.ProcessState.Exited() { + return + } + _ = cmd.Process.Kill() + _, _ = cmd.Process.Wait() + }) + + lines := make(chan string, 1) + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + lines <- scanner.Text() + return + } + close(lines) + }() + + select { + case line, ok := <-lines: + if !ok { + t.Fatalf("OAuth MCP server exited before listening: %s", stderr.String()) + } + const prefix = "Listening: " + if !strings.HasPrefix(line, prefix) { + t.Fatalf("Unexpected OAuth MCP server startup line %q. stderr=%s", line, stderr.String()) + } + return strings.TrimPrefix(line, prefix) + case <-time.After(10 * time.Second): + t.Fatalf("Timed out waiting for OAuth MCP server: %s", stderr.String()) + } + return "" +} + +func fetchOAuthMCPRequests(t *testing.T, baseURL string) []oauthMCPRequest { + t.Helper() + + response, err := http.Get(baseURL + "/__requests") + if err != nil { + t.Fatalf("Failed to fetch OAuth MCP requests: %v", err) + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + t.Fatalf("Failed to fetch OAuth MCP requests: %s", response.Status) + } + var requests []oauthMCPRequest + if err := json.NewDecoder(response.Body).Decode(&requests); err != nil { + t.Fatalf("Failed to decode OAuth MCP requests: %v", err) + } + return requests +} + +func hasAuthorization(requests []oauthMCPRequest, expected string) bool { + for _, request := range requests { + if request.Authorization == nil && expected == "" { + return true + } + if request.Authorization != nil && *request.Authorization == expected { + return true + } + } + return false +} + +func callWhoami(t *testing.T, session *copilot.Session, serverName string, scenario string) { + t.Helper() + + result, err := session.RPC.MCP.Apps().CallTool(t.Context(), &rpc.MCPAppsCallToolRequest{ + OriginServerName: serverName, + ServerName: serverName, + ToolName: "whoami", + Arguments: map[string]any{"scenario": scenario}, + }) + if err != nil { + t.Fatalf("Failed to call whoami for %s: %v", scenario, err) + } + content, ok := (*result)["content"].([]any) + if !ok || len(content) != 1 { + t.Fatalf("Unexpected whoami result: %#v", result) + } +} diff --git a/go/internal/e2e/testharness/context.go b/go/internal/e2e/testharness/context.go index adceb9a746..d56f957278 100644 --- a/go/internal/e2e/testharness/context.go +++ b/go/internal/e2e/testharness/context.go @@ -14,6 +14,7 @@ import ( ) const defaultGitHubToken = "fake-token-for-e2e-tests" +const localRuntimeCLIPath = "/Users/roji/.copilot/repos/copilot-worktrees/copilot-agent-runtime/roji-symmetrical-dollop/dist-cli/index.js" var ( cliPath string @@ -28,6 +29,10 @@ func CLIPath() string { cliPath = path return } + if fileExists(localRuntimeCLIPath) { + cliPath = localRuntimeCLIPath + return + } // Look for CLI in sibling nodejs directory's node_modules. As of CLI // 1.0.64-1 the @github/copilot package is a thin loader; the runnable @@ -223,6 +228,8 @@ func (c *TestContext) Env() []string { "GH_CONFIG_DIR="+c.HomeDir, "GH_TOKEN="+defaultGitHubToken, "GITHUB_TOKEN="+defaultGitHubToken, + "COPILOT_MCP_APPS=true", + "MCP_APPS=true", "XDG_CONFIG_HOME="+c.HomeDir, "XDG_STATE_HOME="+c.HomeDir, ) diff --git a/go/rpc/zrpc.go b/go/rpc/zrpc.go index 03ec16cea1..ee1ac13240 100644 --- a/go/rpc/zrpc.go +++ b/go/rpc/zrpc.go @@ -2617,6 +2617,65 @@ type MCPFilteredServer struct { RedactedReason *string `json:"redactedReason,omitempty"` } +// Host response: supply dynamic headers or decline this refresh. +// Experimental: MCPHeadersHandlePendingHeadersRefreshRequest is part of an experimental API +// and may change or be removed. +type MCPHeadersHandlePendingHeadersRefreshRequest interface { + mcpHeadersHandlePendingHeadersRefreshRequest() + Kind() MCPHeadersHandlePendingHeadersRefreshRequestKind +} + +type RawMCPHeadersHandlePendingHeadersRefreshRequestData struct { + Discriminator MCPHeadersHandlePendingHeadersRefreshRequestKind + Raw json.RawMessage +} + +func (RawMCPHeadersHandlePendingHeadersRefreshRequestData) mcpHeadersHandlePendingHeadersRefreshRequest() { +} +func (r RawMCPHeadersHandlePendingHeadersRefreshRequestData) Kind() MCPHeadersHandlePendingHeadersRefreshRequestKind { + return r.Discriminator +} + +type MCPHeadersHandlePendingHeadersRefreshRequestHeaders struct { + // Headers to overlay onto the MCP request. Dynamic headers override static config headers + // but do not replace SDK-managed request headers. + Headers map[string]string `json:"headers"` +} + +func (MCPHeadersHandlePendingHeadersRefreshRequestHeaders) mcpHeadersHandlePendingHeadersRefreshRequest() { +} +func (MCPHeadersHandlePendingHeadersRefreshRequestHeaders) Kind() MCPHeadersHandlePendingHeadersRefreshRequestKind { + return MCPHeadersHandlePendingHeadersRefreshRequestKindHeaders +} + +type MCPHeadersHandlePendingHeadersRefreshRequestNone struct { +} + +func (MCPHeadersHandlePendingHeadersRefreshRequestNone) mcpHeadersHandlePendingHeadersRefreshRequest() { +} +func (MCPHeadersHandlePendingHeadersRefreshRequestNone) Kind() MCPHeadersHandlePendingHeadersRefreshRequestKind { + return MCPHeadersHandlePendingHeadersRefreshRequestKindNone +} + +// MCP headers refresh request id and the host response. +// Experimental: MCPHeadersHandlePendingHeadersRefreshRequestRequest is part of an +// experimental API and may change or be removed. +type MCPHeadersHandlePendingHeadersRefreshRequestRequest struct { + // Headers refresh request identifier from mcp.headers_refresh_required + RequestID string `json:"requestId"` + // Host response: supply dynamic headers or decline this refresh. + Result MCPHeadersHandlePendingHeadersRefreshRequest `json:"result"` +} + +// Indicates whether the pending MCP headers refresh response was accepted. +// Experimental: MCPHeadersHandlePendingHeadersRefreshRequestResult is part of an +// experimental API and may change or be removed. +type MCPHeadersHandlePendingHeadersRefreshRequestResult struct { + // Whether the response was accepted. False if the request was unknown, timed out, or + // already resolved. + Success bool `json:"success"` +} + // Host-level state, omitted when no MCP host is initialized. // Experimental: MCPHostState is part of an experimental API and may change or be removed. type MCPHostState struct { @@ -2768,8 +2827,6 @@ type MCPOauthPendingRequestResponseToken struct { AccessToken string `json:"accessToken"` // Token lifetime in seconds, if known. ExpiresIn *int64 `json:"expiresIn,omitempty"` - // Refresh token supplied by the host, if available. - RefreshToken *string `json:"refreshToken,omitempty"` // OAuth token type. Defaults to Bearer when omitted. TokenType *string `json:"tokenType,omitempty"` } @@ -3236,6 +3293,10 @@ type Model struct { // Billing information type ModelBilling struct { + // Whole-number percentage discount (0-100) applied to usage billed through this model. + // Populated for the synthetic `auto` model, where requests routed by auto-mode are billed + // at a reduced rate; absent for concrete models. + DiscountPercent *int32 `json:"discountPercent,omitempty"` // Billing cost multiplier relative to the base rate Multiplier *float64 `json:"multiplier,omitempty"` // Token-level pricing information for this model @@ -5641,6 +5702,16 @@ type RemoteSessionRepository struct { Owner string `json:"owner"` } +// Optional experimental response budget limits. +// Experimental: ResponseBudgetConfig is part of an experimental API and may change or be +// removed. +type ResponseBudgetConfig struct { + // Maximum AI Credits allowed while responding to one top-level user message. + MaxAiCredits *float64 `json:"maxAiCredits,omitempty"` + // Maximum model-call iterations allowed while responding to one top-level user message. + MaxModelIterations *int64 `json:"maxModelIterations,omitempty"` +} + type RuntimeShutdownResult struct { } @@ -6565,6 +6636,9 @@ type SessionOpenOptions struct { AdditionalContentExclusionPolicies []SessionOpenOptionsAdditionalContentExclusionPolicy `json:"additionalContentExclusionPolicies,omitzero"` // Runtime context discriminator for agent filtering. AgentContext *string `json:"agentContext,omitempty"` + // Whether to include instructions from every MCP server in the system prompt instead of + // only allowlisted servers. + AllowAllMCPServerInstructions *bool `json:"allowAllMcpServerInstructions,omitempty"` // Whether ask_user is explicitly disabled. AskUserDisabled *bool `json:"askUserDisabled,omitempty"` // Initial authentication info for the session. @@ -6662,6 +6736,8 @@ type SessionOpenOptions struct { RemoteExporting *bool `json:"remoteExporting,omitempty"` // Whether this session supports remote steering. RemoteSteerable *bool `json:"remoteSteerable,omitempty"` + // Initial experimental response budget limits for the session. + ResponseBudget *ResponseBudgetConfig `json:"responseBudget,omitempty"` // Whether the host is an interactive UI. RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` // Resolved sandbox configuration. @@ -7384,6 +7460,9 @@ type SessionUpdateOptionsParams struct { AdditionalContentExclusionPolicies []OptionsUpdateAdditionalContentExclusionPolicy `json:"additionalContentExclusionPolicies,omitzero"` // Runtime context discriminator (e.g., `cli`, `actions`). AgentContext *string `json:"agentContext,omitempty"` + // Whether to include instructions from every MCP server in the system prompt instead of + // only allowlisted servers. + AllowAllMCPServerInstructions *bool `json:"allowAllMcpServerInstructions,omitempty"` // Whether to disable the `ask_user` tool (encourages autonomous behavior). AskUserDisabled *bool `json:"askUserDisabled,omitempty"` // Allowlist of tool names available to this session. @@ -7471,6 +7550,8 @@ type SessionUpdateOptionsParams struct { ReasoningEffort *string `json:"reasoningEffort,omitempty"` // Reasoning summary mode for supported model clients. ReasoningSummary *OptionsUpdateReasoningSummary `json:"reasoningSummary,omitempty"` + // Optional experimental response budget limits. Pass null to clear the response budget. + ResponseBudget *ResponseBudgetConfig `json:"responseBudget,omitempty"` // Whether the session is running in an interactive UI. RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` // Resolved sandbox configuration. @@ -7781,8 +7862,8 @@ type SlashCommandInput struct { Required *bool `json:"required,omitempty"` } -// Result of invoking the slash command (text output, prompt to send to the agent, or -// completion). +// Result of invoking the slash command (text output, prompt to send to the agent, +// completion, or subcommand selection). // Experimental: SlashCommandInvocationResult is part of an experimental API and may change // or be removed. type SlashCommandInvocationResult interface { @@ -7896,6 +7977,11 @@ type SubagentSettings struct { Agents map[string]SubagentSettingsEntry `json:"agents,omitzero"` // Names of subagents the user has turned off; they cannot be dispatched DisabledSubagents []string `json:"disabledSubagents,omitzero"` + // Maximum number of subagents that can run concurrently; applies to usage-based billing + // users only + MaxConcurrency *int32 `json:"maxConcurrency,omitempty"` + // Maximum subagent nesting depth; applies to usage-based billing users only + MaxDepth *int32 `json:"maxDepth,omitempty"` } // Subagent model, reasoning effort, and context tier settings @@ -9760,6 +9846,14 @@ const ( MCPAppsSetHostContextDetailsThemeLight MCPAppsSetHostContextDetailsTheme = "light" ) +// Kind discriminator for MCPHeadersHandlePendingHeadersRefreshRequest. +type MCPHeadersHandlePendingHeadersRefreshRequestKind string + +const ( + MCPHeadersHandlePendingHeadersRefreshRequestKindHeaders MCPHeadersHandlePendingHeadersRefreshRequestKind = "headers" + MCPHeadersHandlePendingHeadersRefreshRequestKindNone MCPHeadersHandlePendingHeadersRefreshRequestKind = "none" +) + // OAuth grant type override for this login. // Experimental: MCPOauthLoginGrantType is part of an experimental API and may change or be // removed. @@ -12933,7 +13027,7 @@ func (a *CommandsAPI) HandlePendingCommand(ctx context.Context, params *Commands // Parameters: Slash command name and optional raw input string to invoke. // // Returns: Result of invoking the slash command (text output, prompt to send to the agent, -// or completion). +// completion, or subcommand selection). func (a *CommandsAPI) Invoke(ctx context.Context, params *CommandsInvokeRequest) (SlashCommandInvocationResult, error) { req := map[string]any{"sessionId": a.sessionID} if params != nil { @@ -13824,6 +13918,41 @@ func (s *MCPAPI) Apps() *MCPAppsAPI { return (*MCPAppsAPI)(s) } +// Experimental: MCPHeadersAPI contains experimental APIs that may change or be removed. +type MCPHeadersAPI sessionAPI + +// HandlePendingHeadersRefreshRequest responds to a pending MCP dynamic headers refresh +// request. Hosts that subscribe to `mcp.headers_refresh_required` use this to provide +// short-lived per-server headers or to indicate that no dynamic headers are available for +// this refresh. +// +// RPC method: session.mcp.headers.handlePendingHeadersRefreshRequest. +// +// Parameters: MCP headers refresh request id and the host response. +// +// Returns: Indicates whether the pending MCP headers refresh response was accepted. +func (a *MCPHeadersAPI) HandlePendingHeadersRefreshRequest(ctx context.Context, params *MCPHeadersHandlePendingHeadersRefreshRequestRequest) (*MCPHeadersHandlePendingHeadersRefreshRequestResult, error) { + req := map[string]any{"sessionId": a.sessionID} + if params != nil { + req["requestId"] = params.RequestID + req["result"] = params.Result + } + raw, err := a.client.Request(ctx, "session.mcp.headers.handlePendingHeadersRefreshRequest", req) + if err != nil { + return nil, err + } + var result MCPHeadersHandlePendingHeadersRefreshRequestResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// Experimental: Headers returns experimental APIs that may change or be removed. +func (s *MCPAPI) Headers() *MCPHeadersAPI { + return (*MCPHeadersAPI)(s) +} + // Experimental: MCPOauthAPI contains experimental APIs that may change or be removed. type MCPOauthAPI sessionAPI @@ -14317,6 +14446,9 @@ func (a *OptionsAPI) Update(ctx context.Context, params *SessionUpdateOptionsPar if params.AgentContext != nil { req["agentContext"] = *params.AgentContext } + if params.AllowAllMCPServerInstructions != nil { + req["allowAllMcpServerInstructions"] = *params.AllowAllMCPServerInstructions + } if params.AskUserDisabled != nil { req["askUserDisabled"] = *params.AskUserDisabled } @@ -14425,6 +14557,9 @@ func (a *OptionsAPI) Update(ctx context.Context, params *SessionUpdateOptionsPar if params.ReasoningSummary != nil { req["reasoningSummary"] = *params.ReasoningSummary } + if params.ResponseBudget != nil { + req["responseBudget"] = *params.ResponseBudget + } if params.RunningInInteractiveMode != nil { req["runningInInteractiveMode"] = *params.RunningInInteractiveMode } diff --git a/go/rpc/zrpc_encoding.go b/go/rpc/zrpc_encoding.go index b4942942b5..35cee28250 100644 --- a/go/rpc/zrpc_encoding.go +++ b/go/rpc/zrpc_encoding.go @@ -1138,6 +1138,89 @@ func (r *MCPConfigUpdateRequest) UnmarshalJSON(data []byte) error { return nil } +func unmarshalMCPHeadersHandlePendingHeadersRefreshRequest(data []byte) (MCPHeadersHandlePendingHeadersRefreshRequest, error) { + if string(data) == "null" { + return nil, nil + } + type rawUnion struct { + Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` + } + var raw rawUnion + if err := json.Unmarshal(data, &raw); err != nil { + return nil, err + } + + switch raw.Kind { + case MCPHeadersHandlePendingHeadersRefreshRequestKindHeaders: + var d MCPHeadersHandlePendingHeadersRefreshRequestHeaders + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + case MCPHeadersHandlePendingHeadersRefreshRequestKindNone: + var d MCPHeadersHandlePendingHeadersRefreshRequestNone + if err := json.Unmarshal(data, &d); err != nil { + return nil, err + } + return &d, nil + default: + return &RawMCPHeadersHandlePendingHeadersRefreshRequestData{Discriminator: raw.Kind, Raw: data}, nil + } +} + +func (r RawMCPHeadersHandlePendingHeadersRefreshRequestData) MarshalJSON() ([]byte, error) { + if r.Raw != nil { + return r.Raw, nil + } + return json.Marshal(struct { + Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` + }{ + Kind: r.Discriminator, + }) +} + +func (r MCPHeadersHandlePendingHeadersRefreshRequestHeaders) MarshalJSON() ([]byte, error) { + type alias MCPHeadersHandlePendingHeadersRefreshRequestHeaders + return json.Marshal(struct { + Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r MCPHeadersHandlePendingHeadersRefreshRequestNone) MarshalJSON() ([]byte, error) { + type alias MCPHeadersHandlePendingHeadersRefreshRequestNone + return json.Marshal(struct { + Kind MCPHeadersHandlePendingHeadersRefreshRequestKind `json:"kind"` + alias + }{ + Kind: r.Kind(), + alias: alias(r), + }) +} + +func (r *MCPHeadersHandlePendingHeadersRefreshRequestRequest) UnmarshalJSON(data []byte) error { + type rawMCPHeadersHandlePendingHeadersRefreshRequestRequest struct { + RequestID string `json:"requestId"` + Result json.RawMessage `json:"result"` + } + var raw rawMCPHeadersHandlePendingHeadersRefreshRequestRequest + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + r.RequestID = raw.RequestID + if raw.Result != nil { + value, err := unmarshalMCPHeadersHandlePendingHeadersRefreshRequest(raw.Result) + if err != nil { + return err + } + r.Result = value + } + return nil +} + func unmarshalMCPOauthPendingRequestResponse(data []byte) (MCPOauthPendingRequestResponse, error) { if string(data) == "null" { return nil, nil @@ -2819,6 +2902,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { type rawSessionOpenOptions struct { AdditionalContentExclusionPolicies []SessionOpenOptionsAdditionalContentExclusionPolicy `json:"additionalContentExclusionPolicies,omitzero"` AgentContext *string `json:"agentContext,omitempty"` + AllowAllMCPServerInstructions *bool `json:"allowAllMcpServerInstructions,omitempty"` AskUserDisabled *bool `json:"askUserDisabled,omitempty"` AuthInfo json.RawMessage `json:"authInfo,omitempty"` AvailableTools []string `json:"availableTools,omitzero"` @@ -2861,6 +2945,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { RemoteDefaultedOn *bool `json:"remoteDefaultedOn,omitempty"` RemoteExporting *bool `json:"remoteExporting,omitempty"` RemoteSteerable *bool `json:"remoteSteerable,omitempty"` + ResponseBudget *ResponseBudgetConfig `json:"responseBudget,omitempty"` RunningInInteractiveMode *bool `json:"runningInInteractiveMode,omitempty"` SandboxConfig *SandboxConfig `json:"sandboxConfig,omitempty"` SessionCapabilities []SessionCapability `json:"sessionCapabilities,omitzero"` @@ -2879,6 +2964,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { } r.AdditionalContentExclusionPolicies = raw.AdditionalContentExclusionPolicies r.AgentContext = raw.AgentContext + r.AllowAllMCPServerInstructions = raw.AllowAllMCPServerInstructions r.AskUserDisabled = raw.AskUserDisabled if raw.AuthInfo != nil { value, err := unmarshalAuthInfo(raw.AuthInfo) @@ -2927,6 +3013,7 @@ func (r *SessionOpenOptions) UnmarshalJSON(data []byte) error { r.RemoteDefaultedOn = raw.RemoteDefaultedOn r.RemoteExporting = raw.RemoteExporting r.RemoteSteerable = raw.RemoteSteerable + r.ResponseBudget = raw.ResponseBudget r.RunningInInteractiveMode = raw.RunningInInteractiveMode r.SandboxConfig = raw.SandboxConfig r.SessionCapabilities = raw.SessionCapabilities diff --git a/go/rpc/zsession_encoding.go b/go/rpc/zsession_encoding.go index 89e9cc26d3..f26dabc269 100644 --- a/go/rpc/zsession_encoding.go +++ b/go/rpc/zsession_encoding.go @@ -41,6 +41,12 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeAssistantIdle: + var d AssistantIdleData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeAssistantIntent: var d AssistantIntentData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -203,6 +209,18 @@ func (e *SessionEvent) UnmarshalJSON(data []byte) error { return err } e.Data = &d + case SessionEventTypeMCPHeadersRefreshCompleted: + var d MCPHeadersRefreshCompletedData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d + case SessionEventTypeMCPHeadersRefreshRequired: + var d MCPHeadersRefreshRequiredData + if err := json.Unmarshal(raw.Data, &d); err != nil { + return err + } + e.Data = &d case SessionEventTypeMCPOauthCompleted: var d MCPOauthCompletedData if err := json.Unmarshal(raw.Data, &d); err != nil { @@ -645,6 +663,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { AgentMode *UserMessageAgentMode `json:"agentMode,omitempty"` Attachments []json.RawMessage `json:"attachments,omitzero"` Content string `json:"content"` + Delivery *UserMessageDelivery `json:"delivery,omitempty"` InteractionID *string `json:"interactionId,omitempty"` IsAutopilotContinuation *bool `json:"isAutopilotContinuation,omitempty"` NativeDocumentPathFallbackPaths []string `json:"nativeDocumentPathFallbackPaths,omitzero"` @@ -669,6 +688,7 @@ func (r *UserMessageData) UnmarshalJSON(data []byte) error { } } r.Content = raw.Content + r.Delivery = raw.Delivery r.InteractionID = raw.InteractionID r.IsAutopilotContinuation = raw.IsAutopilotContinuation r.NativeDocumentPathFallbackPaths = raw.NativeDocumentPathFallbackPaths diff --git a/go/rpc/zsession_events.go b/go/rpc/zsession_events.go index 7964da76bc..df0aa5beea 100644 --- a/go/rpc/zsession_events.go +++ b/go/rpc/zsession_events.go @@ -54,6 +54,7 @@ type SessionEventType string const ( SessionEventTypeAbort SessionEventType = "abort" + SessionEventTypeAssistantIdle SessionEventType = "assistant.idle" SessionEventTypeAssistantIntent SessionEventType = "assistant.intent" SessionEventTypeAssistantMessage SessionEventType = "assistant.message" SessionEventTypeAssistantMessageDelta SessionEventType = "assistant.message_delta" @@ -81,6 +82,8 @@ const ( SessionEventTypeHookProgress SessionEventType = "hook.progress" SessionEventTypeHookStart SessionEventType = "hook.start" SessionEventTypeMCPAppToolCallComplete SessionEventType = "mcp_app.tool_call_complete" + SessionEventTypeMCPHeadersRefreshCompleted SessionEventType = "mcp.headers_refresh_completed" + SessionEventTypeMCPHeadersRefreshRequired SessionEventType = "mcp.headers_refresh_required" SessionEventTypeMCPOauthCompleted SessionEventType = "mcp.oauth_completed" SessionEventTypeMCPOauthRequired SessionEventType = "mcp.oauth_required" SessionEventTypeModelCallFailure SessionEventType = "model.call_failure" @@ -453,6 +456,23 @@ type SessionCanvasRemovedData struct { func (*SessionCanvasRemovedData) sessionEventData() {} func (*SessionCanvasRemovedData) Type() SessionEventType { return SessionEventTypeSessionCanvasRemoved } +// Dynamic headers refresh request for a remote MCP server +type MCPHeadersRefreshRequiredData struct { + // Why dynamic headers are being requested. + Reason MCPHeadersRefreshRequiredReason `json:"reason"` + // Unique identifier for this headers refresh request; used to respond via session.mcp.headers.handlePendingHeadersRefreshRequest() + RequestID string `json:"requestId"` + // Display name of the remote MCP server requesting headers + ServerName string `json:"serverName"` + // URL of the remote MCP server requesting headers + ServerURL string `json:"serverUrl"` +} + +func (*MCPHeadersRefreshRequiredData) sessionEventData() {} +func (*MCPHeadersRefreshRequiredData) Type() SessionEventType { + return SessionEventTypeMCPHeadersRefreshRequired +} + // Elicitation request completion with the user's response type ElicitationCompletedData struct { // The user action: "accept" (submitted form), "decline" (explicitly refused), or "cancel" (dismissed) @@ -744,6 +764,19 @@ type MCPOauthCompletedData struct { func (*MCPOauthCompletedData) sessionEventData() {} func (*MCPOauthCompletedData) Type() SessionEventType { return SessionEventTypeMCPOauthCompleted } +// MCP headers refresh request completion notification +type MCPHeadersRefreshCompletedData struct { + // How the pending MCP headers refresh request resolved. + Outcome MCPHeadersRefreshCompletedOutcome `json:"outcome"` + // Request ID of the resolved headers refresh request + RequestID string `json:"requestId"` +} + +func (*MCPHeadersRefreshCompletedData) sessionEventData() {} +func (*MCPHeadersRefreshCompletedData) Type() SessionEventType { + return SessionEventTypeMCPHeadersRefreshCompleted +} + // Model change details including previous and new model identifiers type SessionModelChangeData struct { // Reason the change happened, when not user-initiated. Currently `"rate_limit_auto_switch"` for changes triggered by the auto-mode-switch rate-limit recovery path. UI clients can use this to render contextual copy. @@ -780,6 +813,8 @@ func (*SessionRemoteSteerableChangedData) Type() SessionEventType { // OAuth authentication request for an MCP server type MCPOauthRequiredData struct { + // Why the runtime is requesting host-provided OAuth credentials. + Reason MCPOauthRequestReason `json:"reason"` // Unique identifier for this OAuth request; used to respond via session.mcp.oauth.handlePendingRequest RequestID string `json:"requestId"` // Raw OAuth protected-resource metadata document fetched for the MCP server, if available @@ -816,6 +851,15 @@ func (*SessionCustomNotificationData) Type() SessionEventType { return SessionEventTypeSessionCustomNotification } +// Payload emitted whenever the main agent's processing loop goes idle, including while related background work (running agents or in-flight attached shell commands) is still pending and the session-level idle event is therefore deferred +type AssistantIdleData struct { + // True when the preceding agentic loop was cancelled via abort signal + Aborted *bool `json:"aborted,omitempty"` +} + +func (*AssistantIdleData) sessionEventData() {} +func (*AssistantIdleData) Type() SessionEventType { return SessionEventTypeAssistantIdle } + // Payload indicating the session is idle with no background agents or attached shell commands in flight type SessionIdleData struct { // True when the preceding agentic loop was cancelled via abort signal @@ -1165,6 +1209,8 @@ type UserMessageData struct { Attachments []Attachment `json:"attachments,omitzero"` // The user's message text as displayed in the timeline Content string `json:"content"` + // How this message was delivered to the agentic loop relative to loop state (idle-start vs. steering/queued while busy). The timing axis; combine with `source` (origin) for the full picture. Used for telemetry attribution. + Delivery *UserMessageDelivery `json:"delivery,omitempty"` // CAPI interaction ID for correlating this user message with its turn InteractionID *string `json:"interactionId,omitempty"` // True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. @@ -1247,6 +1293,8 @@ type SessionStartData struct { ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` // Whether this session supports remote steering via GitHub RemoteSteerable *bool `json:"remoteSteerable,omitempty"` + // Response budget limits configured at session creation time, if any + ResponseBudget *ResponseBudgetConfig `json:"responseBudget,omitempty"` // Model selected at session creation time, if any SelectedModel *string `json:"selectedModel,omitempty"` // Unique identifier for the session @@ -1280,6 +1328,8 @@ type SessionResumeData struct { ReasoningSummary *ReasoningSummary `json:"reasoningSummary,omitempty"` // Whether this session supports remote steering via GitHub RemoteSteerable *bool `json:"remoteSteerable,omitempty"` + // Response budget limits currently configured at resume time; null when no budget is active + ResponseBudget *ResponseBudgetConfig `json:"responseBudget,omitempty"` // ISO 8601 timestamp when the session was resumed ResumeTime time.Time `json:"resumeTime"` // Model currently selected at resume time @@ -1610,6 +1660,8 @@ type ToolExecutionStartData struct { // Tool call ID of the parent tool invocation when this event originates from a sub-agent // Deprecated: ParentToolCallID is deprecated. ParentToolCallID *string `json:"parentToolCallId,omitempty"` + // Shell-tool path hints derived from the command at start time for shell tools (bash/powershell/local_shell). Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. Absent for non-shell tools. + ShellToolInfo *ToolExecutionStartShellToolInfo `json:"shellToolInfo,omitempty"` // Unique identifier for this tool call ToolCallID string `json:"toolCallId"` // Tool definition metadata, present for MCP tools with MCP Apps support @@ -2145,6 +2197,8 @@ type MCPAppToolCallCompleteToolMetaUI struct { type MCPOauthRequiredStaticClientConfig struct { // OAuth client ID for the server ClientID string `json:"clientId"` + // Optional OAuth client secret for confidential static clients, when the runtime can resolve one + ClientSecret *string `json:"clientSecret,omitempty"` // Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). GrantType *MCPOauthRequiredStaticClientConfigGrantType `json:"grantType,omitempty"` // Whether this is a public OAuth client @@ -2155,8 +2209,8 @@ type MCPOauthRequiredStaticClientConfig struct { type MCPOauthWwwAuthenticateParams struct { // OAuth error from the WWW-Authenticate error parameter, if present Error *string `json:"error,omitempty"` - // Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter - ResourceMetadataURL string `json:"resourceMetadataUrl"` + // Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter, if present + ResourceMetadataURL *string `json:"resourceMetadataUrl,omitempty"` // Requested OAuth scopes from the WWW-Authenticate scope parameter, if present Scope *string `json:"scope,omitempty"` } @@ -3264,6 +3318,14 @@ type ToolExecutionCompleteUIResourceMetaUIPermissionsGeolocation struct { type ToolExecutionCompleteUIResourceMetaUIPermissionsMicrophone struct { } +// Shell-aware path hints for a shell tool's command, captured at start time so consumers can snapshot a file's pre-image before the tool runs. +type ToolExecutionStartShellToolInfo struct { + // Whether the command includes a file write redirection (e.g., > or >>). + HasWriteFileRedirection bool `json:"hasWriteFileRedirection"` + // File paths the command may read or write, derived from the command at start time. Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. + PossiblePaths []string `json:"possiblePaths"` +} + // Tool definition metadata, present for MCP tools with MCP Apps support type ToolExecutionStartToolDescription struct { // Tool description @@ -3494,6 +3556,30 @@ const ( HandoffSourceTypeRemote HandoffSourceType = "remote" ) +// How the pending MCP headers refresh request resolved. +type MCPHeadersRefreshCompletedOutcome string + +const ( + // The host supplied dynamic headers. + MCPHeadersRefreshCompletedOutcomeHeaders MCPHeadersRefreshCompletedOutcome = "headers" + // The host responded with no dynamic headers. + MCPHeadersRefreshCompletedOutcomeNone MCPHeadersRefreshCompletedOutcome = "none" + // No response arrived within the bounded window. + MCPHeadersRefreshCompletedOutcomeTimeout MCPHeadersRefreshCompletedOutcome = "timeout" +) + +// Why dynamic headers are being requested. +type MCPHeadersRefreshRequiredReason string + +const ( + // The server returned 401 and stale dynamic headers were invalidated. + MCPHeadersRefreshRequiredReasonAuthFailed MCPHeadersRefreshRequiredReason = "auth-failed" + // The transport is making its first dynamic header request for this server. + MCPHeadersRefreshRequiredReasonStartup MCPHeadersRefreshRequiredReason = "startup" + // The previously cached dynamic headers expired. + MCPHeadersRefreshRequiredReasonTtlExpired MCPHeadersRefreshRequiredReason = "ttl-expired" +) + // How the pending MCP OAuth request was completed type MCPOauthCompletionOutcome string @@ -3504,6 +3590,20 @@ const ( MCPOauthCompletionOutcomeToken MCPOauthCompletionOutcome = "token" ) +// Reason the runtime is requesting host-provided MCP OAuth credentials +type MCPOauthRequestReason string + +const ( + // Initial credentials are required before connecting to the MCP server. + MCPOauthRequestReasonInitial MCPOauthRequestReason = "initial" + // The server requires a new host authorization flow before continuing. + MCPOauthRequestReasonReauth MCPOauthRequestReason = "reauth" + // The current host-provided credential was rejected and a replacement is requested. + MCPOauthRequestReasonRefresh MCPOauthRequestReason = "refresh" + // The server requires a credential with additional scope or audience. + MCPOauthRequestReasonUpscope MCPOauthRequestReason = "upscope" +) + // Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). type MCPOauthRequiredStaticClientConfigGrantType string @@ -3768,6 +3868,18 @@ const ( UserMessageAgentModeShell UserMessageAgentMode = "shell" ) +// How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn. +type UserMessageDelivery string + +const ( + // Delivered while the loop was idle; starts its own run immediately (a human's fresh turn, or a system notification waking an idle agent). + UserMessageDeliveryIdle UserMessageDelivery = "idle" + // Enqueued while the agent was busy; processed as its own run afterward. + UserMessageDeliveryQueued UserMessageDelivery = "queued" + // Injected into the current in-flight run while the agent was busy (immediate mode). + UserMessageDeliverySteering UserMessageDelivery = "steering" +) + // Hosting platform type of the repository (github or ado) type WorkingDirectoryContextHostType string diff --git a/go/session.go b/go/session.go index 851157ba87..2706aa81a6 100644 --- a/go/session.go +++ b/go/session.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "log" "sync" "time" @@ -61,6 +62,8 @@ type Session struct { toolHandlersM sync.RWMutex permissionHandler PermissionHandlerFunc permissionMux sync.RWMutex + mcpAuthHandler MCPAuthHandler + mcpAuthMu sync.RWMutex userInputHandler UserInputHandler userInputMux sync.RWMutex exitPlanModeHandler ExitPlanModeRequestHandler @@ -924,6 +927,45 @@ func (s *Session) getElicitationHandler() ElicitationHandler { return s.elicitationHandler } +func (s *Session) registerMCPAuthHandler(handler MCPAuthHandler) { + s.mcpAuthMu.Lock() + defer s.mcpAuthMu.Unlock() + s.mcpAuthHandler = handler +} + +func (s *Session) getMCPAuthHandler() MCPAuthHandler { + s.mcpAuthMu.RLock() + defer s.mcpAuthMu.RUnlock() + return s.mcpAuthHandler +} + +func (s *Session) handleMCPAuthRequest(request MCPAuthRequest) { + handler := s.getMCPAuthHandler() + if handler == nil { + return + } + + ctx := context.Background() + cancel := &rpc.MCPOauthPendingRequestResponseCancelled{} + result, err := handler(request, MCPAuthInvocation{SessionID: s.SessionID}) + if err != nil || result == nil || result.Kind == "cancelled" || result.Token == nil { + s.RPC.MCP.Oauth().HandlePendingRequest(ctx, &rpc.MCPOauthHandlePendingRequest{ + RequestID: request.RequestID, + Result: cancel, + }) + return + } + + s.RPC.MCP.Oauth().HandlePendingRequest(ctx, &rpc.MCPOauthHandlePendingRequest{ + RequestID: request.RequestID, + Result: &rpc.MCPOauthPendingRequestResponseToken{ + AccessToken: result.Token.AccessToken, + TokenType: result.Token.TokenType, + ExpiresIn: result.Token.ExpiresIn, + }, + }) +} + // handleElicitationRequest dispatches an elicitation.requested event to the registered handler // and sends the result back via the RPC layer. Auto-cancels on error. func (s *Session) handleElicitationRequest(elicitCtx ElicitationContext, requestID string) { @@ -1370,6 +1412,60 @@ func (s *Session) handleBroadcastEvent(event SessionEvent) { } s.executePermissionAndRespond(d.RequestID, d.PermissionRequest, handler) + case *MCPOauthRequiredData: + handler := s.getMCPAuthHandler() + if d.RequestID == "" { + return + } + if handler == nil { + log.Printf( + "Received MCP OAuth request without a registered MCP auth handler. SessionId=%s, RequestId=%s", + s.SessionID, + d.RequestID, + ) + return + } + var staticClientConfig *MCPAuthStaticClientConfig + if d.StaticClientConfig != nil { + var grantType string + if d.StaticClientConfig.GrantType != nil { + grantType = string(*d.StaticClientConfig.GrantType) + } + staticClientConfig = &MCPAuthStaticClientConfig{ + ClientID: d.StaticClientConfig.ClientID, + GrantType: grantType, + PublicClient: d.StaticClientConfig.PublicClient, + } + if d.StaticClientConfig.ClientSecret != nil { + staticClientConfig.ClientSecret = *d.StaticClientConfig.ClientSecret + } + } + request := MCPAuthRequest{ + RequestID: d.RequestID, + ServerName: d.ServerName, + ServerURL: d.ServerURL, + Reason: string(d.Reason), + StaticClientConfig: staticClientConfig, + } + if d.ResourceMetadata != nil { + request.ResourceMetadata = *d.ResourceMetadata + } + if d.WwwAuthenticateParams != nil { + var scope, oauthError string + if d.WwwAuthenticateParams.Scope != nil { + scope = *d.WwwAuthenticateParams.Scope + } + if d.WwwAuthenticateParams.Error != nil { + oauthError = *d.WwwAuthenticateParams.Error + } + request.WwwAuthenticateParams = &MCPAuthWwwAuthenticateParams{ + ResourceMetadataURL: d.WwwAuthenticateParams.ResourceMetadataURL, + Scope: scope, + Error: oauthError, + } + } + s.handleMCPAuthRequest(request) + case *CommandExecuteData: s.executeCommandAndRespond(d.RequestID, d.CommandName, d.Command, d.Args) diff --git a/go/session_test.go b/go/session_test.go index 654be6ce46..078001cdbb 100644 --- a/go/session_test.go +++ b/go/session_test.go @@ -60,6 +60,205 @@ func TestSession_SetModelOmitsContextTierWhenUnset(t *testing.T) { } } +func TestSession_MCPAuthRequestSendsHostToken(t *testing.T) { + stdinR, stdinW := io.Pipe() + stdoutR, stdoutW := io.Pipe() + defer stdinR.Close() + defer stdinW.Close() + defer stdoutR.Close() + defer stdoutW.Close() + + client := jsonrpc2.NewClient(stdinW, stdoutR) + client.Start() + defer client.Stop() + + paramsCh := make(chan map[string]any, 1) + errCh := make(chan error, 1) + + go func() { + frame, err := readTestJSONRPCFrame(stdinR) + if err != nil { + errCh <- err + return + } + + var request struct { + ID json.RawMessage `json:"id"` + Method string `json:"method"` + Params map[string]any `json:"params"` + } + if err := json.Unmarshal(frame, &request); err != nil { + errCh <- err + return + } + if request.Method != "session.mcp.oauth.handlePendingRequest" { + errCh <- fmt.Errorf("expected session.mcp.oauth.handlePendingRequest, got %s", request.Method) + return + } + + paramsCh <- request.Params + + response := map[string]any{ + "jsonrpc": "2.0", + "id": json.RawMessage(request.ID), + "result": map[string]any{"success": true}, + } + data, err := json.Marshal(response) + if err != nil { + errCh <- err + return + } + if _, err := fmt.Fprintf(stdoutW, "Content-Length: %d\r\n\r\n%s", len(data), data); err != nil { + errCh <- err + } + }() + + session := &Session{ + SessionID: "session-1", + client: client, + RPC: rpc.NewSessionRPC(client, "session-1"), + } + var observedRequest MCPAuthRequest + session.registerMCPAuthHandler(func(request MCPAuthRequest, invocation MCPAuthInvocation) (*MCPAuthResult, error) { + observedRequest = request + if invocation.SessionID != "session-1" { + t.Fatalf("expected invocation session-1, got %s", invocation.SessionID) + } + if request.RequestID != "oauth-request" { + t.Fatalf("expected oauth-request, got %s", request.RequestID) + } + tokenType := "Bearer" + return &MCPAuthResult{ + Kind: "token", + Token: &MCPAuthToken{ + AccessToken: "host-token", + TokenType: &tokenType, + }, + }, nil + }) + resourceMetadataURL := "https://example.com/.well-known/oauth-protected-resource" + resourceMetadata := `{"resource":"https://example.com/mcp"}` + clientSecret := "static-secret" + grantType := rpc.MCPOauthRequiredStaticClientConfigGrantTypeClientCredentials + publicClient := false + session.handleBroadcastEvent(SessionEvent{ + Data: &MCPOauthRequiredData{ + RequestID: "oauth-request", + Reason: rpc.MCPOauthRequestReasonInitial, + ServerName: "oauth-server", + ServerURL: "https://example.com/mcp", + ResourceMetadata: &resourceMetadata, + StaticClientConfig: &MCPOauthRequiredStaticClientConfig{ + ClientID: "static-client", + ClientSecret: &clientSecret, + GrantType: &grantType, + PublicClient: &publicClient, + }, + WwwAuthenticateParams: &MCPOauthWwwAuthenticateParams{ + ResourceMetadataURL: &resourceMetadataURL, + }, + }, + }) + if observedRequest.ResourceMetadata != `{"resource":"https://example.com/mcp"}` { + t.Fatalf("expected resource metadata to be propagated, got %q", observedRequest.ResourceMetadata) + } + if observedRequest.Reason != "initial" { + t.Fatalf("expected initial reason, got %q", observedRequest.Reason) + } + if observedRequest.WwwAuthenticateParams == nil { + t.Fatal("expected WWW-Authenticate params to be propagated") + } + if observedRequest.StaticClientConfig == nil { + t.Fatal("expected static client config to be propagated") + } + if observedRequest.StaticClientConfig.ClientSecret != "static-secret" { + t.Fatalf("expected static client secret to be propagated, got %q", observedRequest.StaticClientConfig.ClientSecret) + } + + select { + case params := <-paramsCh: + if params["sessionId"] != "session-1" { + t.Fatalf("expected sessionId session-1, got %v", params["sessionId"]) + } + if params["requestId"] != "oauth-request" { + t.Fatalf("expected requestId oauth-request, got %v", params["requestId"]) + } + result, ok := params["result"].(map[string]any) + if !ok { + t.Fatalf("expected result object, got %T", params["result"]) + } + if result["kind"] != "token" { + t.Fatalf("expected token kind, got %v", result["kind"]) + } + if result["accessToken"] != "host-token" { + t.Fatalf("expected accessToken host-token, got %v", result["accessToken"]) + } + if result["tokenType"] != "Bearer" { + t.Fatalf("expected tokenType Bearer, got %v", result["tokenType"]) + } + case err := <-errCh: + t.Fatal(err) + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for MCP OAuth request") + } +} + +func TestMCPAuthRequestAllowsMissingOptionalMetadata(t *testing.T) { + request := MCPAuthRequest{RequestID: "oauth-request"} + if request.ResourceMetadata != "" { + t.Fatalf("expected no resource metadata, got %q", request.ResourceMetadata) + } + if request.WwwAuthenticateParams != nil { + t.Fatalf("expected no WWW-Authenticate params, got %#v", request.WwwAuthenticateParams) + } +} + +func TestMCPOauthRequiredDataAllowsOptionalMetadata(t *testing.T) { + var withMetadata rpc.MCPOauthRequiredData + if err := json.Unmarshal([]byte(`{ + "requestId": "oauth-request", + "reason": "initial", + "serverName": "oauth-server", + "serverUrl": "https://example.com/mcp", + "wwwAuthenticateParams": { + "resourceMetadataUrl": "https://example.com/.well-known/oauth-protected-resource" + }, + "resourceMetadata": "{\"resource\":\"https://example.com/mcp\"}", + "staticClientConfig": { + "clientId": "static-client", + "clientSecret": "static-secret", + "publicClient": false + } + }`), &withMetadata); err != nil { + t.Fatal(err) + } + if withMetadata.ResourceMetadata == nil || *withMetadata.ResourceMetadata != `{"resource":"https://example.com/mcp"}` { + t.Fatalf("expected resource metadata, got %#v", withMetadata.ResourceMetadata) + } + if withMetadata.WwwAuthenticateParams == nil { + t.Fatal("expected WWW-Authenticate params") + } + if withMetadata.StaticClientConfig == nil || withMetadata.StaticClientConfig.ClientSecret == nil || *withMetadata.StaticClientConfig.ClientSecret != "static-secret" { + t.Fatalf("expected static client secret, got %#v", withMetadata.StaticClientConfig) + } + + var withoutMetadata rpc.MCPOauthRequiredData + if err := json.Unmarshal([]byte(`{ + "requestId": "oauth-request", + "reason": "initial", + "serverName": "oauth-server", + "serverUrl": "https://example.com/mcp" + }`), &withoutMetadata); err != nil { + t.Fatal(err) + } + if withoutMetadata.ResourceMetadata != nil { + t.Fatalf("expected no resource metadata, got %#v", withoutMetadata.ResourceMetadata) + } + if withoutMetadata.WwwAuthenticateParams != nil { + t.Fatalf("expected no WWW-Authenticate params, got %#v", withoutMetadata.WwwAuthenticateParams) + } +} + func captureSetModelRequest(t *testing.T, opts *SetModelOptions) map[string]any { t.Helper() diff --git a/go/types.go b/go/types.go index 8a7df3c46a..fd623ab935 100644 --- a/go/types.go +++ b/go/types.go @@ -326,6 +326,53 @@ type PermissionInvocation struct { SessionID string } +// MCPAuthWwwAuthenticateParams contains parsed parameters from an MCP server's WWW-Authenticate response. +type MCPAuthWwwAuthenticateParams struct { + ResourceMetadataURL *string `json:"resourceMetadataUrl,omitempty"` + Scope string `json:"scope,omitempty"` + Error string `json:"error,omitempty"` +} + +// MCPAuthStaticClientConfig is static OAuth client configuration supplied by an MCP server. +type MCPAuthStaticClientConfig struct { + ClientID string `json:"clientId"` + ClientSecret string `json:"clientSecret,omitempty"` + GrantType string `json:"grantType,omitempty"` + PublicClient *bool `json:"publicClient,omitempty"` +} + +// MCPAuthRequest describes an MCP OAuth request that the SDK host can satisfy with a token. +type MCPAuthRequest struct { + RequestID string `json:"requestId"` + ServerName string `json:"serverName"` + ServerURL string `json:"serverUrl"` + Reason string `json:"reason"` + WwwAuthenticateParams *MCPAuthWwwAuthenticateParams `json:"wwwAuthenticateParams,omitempty"` + ResourceMetadata string `json:"resourceMetadata,omitempty"` + StaticClientConfig *MCPAuthStaticClientConfig `json:"staticClientConfig,omitempty"` +} + +// MCPAuthToken is host-provided OAuth token data for a pending MCP OAuth request. +type MCPAuthToken struct { + AccessToken string `json:"accessToken"` + TokenType *string `json:"tokenType,omitempty"` + ExpiresIn *int64 `json:"expiresIn,omitempty"` +} + +// MCPAuthResult is the result returned by an MCP auth request handler. +type MCPAuthResult struct { + Kind string + Token *MCPAuthToken +} + +// MCPAuthInvocation provides context about an MCP auth handler invocation. +type MCPAuthInvocation struct { + SessionID string +} + +// MCPAuthHandler handles MCP OAuth requests from the runtime. +type MCPAuthHandler func(request MCPAuthRequest, invocation MCPAuthInvocation) (*MCPAuthResult, error) + // UserInputRequest represents a request for user input from the agent type UserInputRequest struct { Question string @@ -975,6 +1022,10 @@ type SessionConfig struct { // When nil, permission requests are surfaced as events and left pending for the // consumer to resolve via pending permission RPCs. OnPermissionRequest PermissionHandlerFunc + // OnMCPAuthRequest is an optional handler for MCP OAuth requests from MCP servers. + // When provided, the SDK can satisfy MCP server OAuth requests with host-provided + // token data or cancellation. + OnMCPAuthRequest MCPAuthHandler // OnUserInputRequest is a handler for user input requests from the agent (enables ask_user tool) OnUserInputRequest UserInputHandler // Hooks configures hook handlers for session lifecycle events @@ -1405,6 +1456,9 @@ type ResumeSessionConfig struct { // When nil, permission requests are surfaced as events and left pending for the // consumer to resolve via pending permission RPCs. OnPermissionRequest PermissionHandlerFunc + // OnMCPAuthRequest is an optional handler for MCP OAuth requests from MCP servers. + // See SessionConfig.OnMCPAuthRequest. + OnMCPAuthRequest MCPAuthHandler // OnUserInputRequest is a handler for user input requests from the agent (enables ask_user tool) OnUserInputRequest UserInputHandler // Hooks configures hook handlers for session lifecycle events diff --git a/go/zsession_events.go b/go/zsession_events.go index 75a22cc997..83ac901d6e 100644 --- a/go/zsession_events.go +++ b/go/zsession_events.go @@ -9,6 +9,7 @@ import "github.com/github/copilot-sdk/go/rpc" type ( AbortData = rpc.AbortData AbortReason = rpc.AbortReason + AssistantIdleData = rpc.AssistantIdleData AssistantIntentData = rpc.AssistantIntentData AssistantMessageData = rpc.AssistantMessageData AssistantMessageDeltaData = rpc.AssistantMessageDeltaData @@ -96,8 +97,13 @@ type ( MCPAppToolCallCompleteError = rpc.MCPAppToolCallCompleteError MCPAppToolCallCompleteToolMeta = rpc.MCPAppToolCallCompleteToolMeta MCPAppToolCallCompleteToolMetaUI = rpc.MCPAppToolCallCompleteToolMetaUI + MCPHeadersRefreshCompletedData = rpc.MCPHeadersRefreshCompletedData + MCPHeadersRefreshCompletedOutcome = rpc.MCPHeadersRefreshCompletedOutcome + MCPHeadersRefreshRequiredData = rpc.MCPHeadersRefreshRequiredData + MCPHeadersRefreshRequiredReason = rpc.MCPHeadersRefreshRequiredReason MCPOauthCompletedData = rpc.MCPOauthCompletedData MCPOauthCompletionOutcome = rpc.MCPOauthCompletionOutcome + MCPOauthRequestReason = rpc.MCPOauthRequestReason MCPOauthRequiredData = rpc.MCPOauthRequiredData MCPOauthRequiredStaticClientConfig = rpc.MCPOauthRequiredStaticClientConfig MCPOauthRequiredStaticClientConfigGrantType = rpc.MCPOauthRequiredStaticClientConfigGrantType @@ -174,6 +180,7 @@ type ( RawSystemNotification = rpc.RawSystemNotification RawToolExecutionCompleteContent = rpc.RawToolExecutionCompleteContent ReasoningSummary = rpc.ReasoningSummary + ResponseBudgetConfig = rpc.ResponseBudgetConfig SamplingCompletedData = rpc.SamplingCompletedData SamplingRequestedData = rpc.SamplingRequestedData SessionAutopilotObjectiveChangedData = rpc.SessionAutopilotObjectiveChangedData @@ -282,6 +289,7 @@ type ( ToolExecutionPartialResultData = rpc.ToolExecutionPartialResultData ToolExecutionProgressData = rpc.ToolExecutionProgressData ToolExecutionStartData = rpc.ToolExecutionStartData + ToolExecutionStartShellToolInfo = rpc.ToolExecutionStartShellToolInfo ToolExecutionStartToolDescription = rpc.ToolExecutionStartToolDescription ToolExecutionStartToolDescriptionMeta = rpc.ToolExecutionStartToolDescriptionMeta ToolExecutionStartToolDescriptionMetaUI = rpc.ToolExecutionStartToolDescriptionMetaUI @@ -291,6 +299,7 @@ type ( UserInputRequestedData = rpc.UserInputRequestedData UserMessageAgentMode = rpc.UserMessageAgentMode UserMessageData = rpc.UserMessageData + UserMessageDelivery = rpc.UserMessageDelivery UserToolSessionApproval = rpc.UserToolSessionApproval UserToolSessionApprovalCommands = rpc.UserToolSessionApprovalCommands UserToolSessionApprovalCustomTool = rpc.UserToolSessionApprovalCustomTool @@ -368,8 +377,18 @@ const ( ExtensionsLoadedExtensionStatusStarting = rpc.ExtensionsLoadedExtensionStatusStarting HandoffSourceTypeLocal = rpc.HandoffSourceTypeLocal HandoffSourceTypeRemote = rpc.HandoffSourceTypeRemote + MCPHeadersRefreshCompletedOutcomeHeaders = rpc.MCPHeadersRefreshCompletedOutcomeHeaders + MCPHeadersRefreshCompletedOutcomeNone = rpc.MCPHeadersRefreshCompletedOutcomeNone + MCPHeadersRefreshCompletedOutcomeTimeout = rpc.MCPHeadersRefreshCompletedOutcomeTimeout + MCPHeadersRefreshRequiredReasonAuthFailed = rpc.MCPHeadersRefreshRequiredReasonAuthFailed + MCPHeadersRefreshRequiredReasonStartup = rpc.MCPHeadersRefreshRequiredReasonStartup + MCPHeadersRefreshRequiredReasonTtlExpired = rpc.MCPHeadersRefreshRequiredReasonTtlExpired MCPOauthCompletionOutcomeCancelled = rpc.MCPOauthCompletionOutcomeCancelled MCPOauthCompletionOutcomeToken = rpc.MCPOauthCompletionOutcomeToken + MCPOauthRequestReasonInitial = rpc.MCPOauthRequestReasonInitial + MCPOauthRequestReasonReauth = rpc.MCPOauthRequestReasonReauth + MCPOauthRequestReasonRefresh = rpc.MCPOauthRequestReasonRefresh + MCPOauthRequestReasonUpscope = rpc.MCPOauthRequestReasonUpscope MCPOauthRequiredStaticClientConfigGrantTypeClientCredentials = rpc.MCPOauthRequiredStaticClientConfigGrantTypeClientCredentials MCPServerSourceBuiltin = rpc.MCPServerSourceBuiltin MCPServerSourcePlugin = rpc.MCPServerSourcePlugin @@ -442,6 +461,7 @@ const ( ReasoningSummaryDetailed = rpc.ReasoningSummaryDetailed ReasoningSummaryNone = rpc.ReasoningSummaryNone SessionEventTypeAbort = rpc.SessionEventTypeAbort + SessionEventTypeAssistantIdle = rpc.SessionEventTypeAssistantIdle SessionEventTypeAssistantIntent = rpc.SessionEventTypeAssistantIntent SessionEventTypeAssistantMessage = rpc.SessionEventTypeAssistantMessage SessionEventTypeAssistantMessageDelta = rpc.SessionEventTypeAssistantMessageDelta @@ -469,6 +489,8 @@ const ( SessionEventTypeHookProgress = rpc.SessionEventTypeHookProgress SessionEventTypeHookStart = rpc.SessionEventTypeHookStart SessionEventTypeMCPAppToolCallComplete = rpc.SessionEventTypeMCPAppToolCallComplete + SessionEventTypeMCPHeadersRefreshCompleted = rpc.SessionEventTypeMCPHeadersRefreshCompleted + SessionEventTypeMCPHeadersRefreshRequired = rpc.SessionEventTypeMCPHeadersRefreshRequired SessionEventTypeMCPOauthCompleted = rpc.SessionEventTypeMCPOauthCompleted SessionEventTypeMCPOauthRequired = rpc.SessionEventTypeMCPOauthRequired SessionEventTypeModelCallFailure = rpc.SessionEventTypeModelCallFailure @@ -577,6 +599,9 @@ const ( UserMessageAgentModeInteractive = rpc.UserMessageAgentModeInteractive UserMessageAgentModePlan = rpc.UserMessageAgentModePlan UserMessageAgentModeShell = rpc.UserMessageAgentModeShell + UserMessageDeliveryIdle = rpc.UserMessageDeliveryIdle + UserMessageDeliveryQueued = rpc.UserMessageDeliveryQueued + UserMessageDeliverySteering = rpc.UserMessageDeliverySteering UserToolSessionApprovalKindCommands = rpc.UserToolSessionApprovalKindCommands UserToolSessionApprovalKindCustomTool = rpc.UserToolSessionApprovalKindCustomTool UserToolSessionApprovalKindExtensionManagement = rpc.UserToolSessionApprovalKindExtensionManagement diff --git a/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java b/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java new file mode 100644 index 0000000000..3b79b8d50e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.idle". Payload emitted whenever the main agent's processing loop goes idle, including while related background work (running agents or in-flight attached shell commands) is still pending and the session-level idle event is therefore deferred + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantIdleEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.idle"; } + + @JsonProperty("data") + private AssistantIdleEventData data; + + public AssistantIdleEventData getData() { return data; } + public void setData(AssistantIdleEventData data) { this.data = data; } + + /** Data payload for {@link AssistantIdleEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantIdleEventData( + /** True when the preceding agentic loop was cancelled via abort signal */ + @JsonProperty("aborted") Boolean aborted + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java new file mode 100644 index 0000000000..a3ba903aea --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "mcp.headers_refresh_completed". MCP headers refresh request completion notification + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpHeadersRefreshCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "mcp.headers_refresh_completed"; } + + @JsonProperty("data") + private McpHeadersRefreshCompletedEventData data; + + public McpHeadersRefreshCompletedEventData getData() { return data; } + public void setData(McpHeadersRefreshCompletedEventData data) { this.data = data; } + + /** Data payload for {@link McpHeadersRefreshCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record McpHeadersRefreshCompletedEventData( + /** Request ID of the resolved headers refresh request */ + @JsonProperty("requestId") String requestId, + /** How the pending MCP headers refresh request resolved. */ + @JsonProperty("outcome") McpHeadersRefreshCompletedOutcome outcome + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java new file mode 100644 index 0000000000..7980dd0a67 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * How the pending MCP headers refresh request resolved. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpHeadersRefreshCompletedOutcome { + /** The {@code headers} variant. */ + HEADERS("headers"), + /** The {@code none} variant. */ + NONE("none"), + /** The {@code timeout} variant. */ + TIMEOUT("timeout"); + + private final String value; + McpHeadersRefreshCompletedOutcome(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpHeadersRefreshCompletedOutcome fromValue(String value) { + for (McpHeadersRefreshCompletedOutcome v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpHeadersRefreshCompletedOutcome value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java new file mode 100644 index 0000000000..d8774bb326 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "mcp.headers_refresh_required". Dynamic headers refresh request for a remote MCP server + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpHeadersRefreshRequiredEvent extends SessionEvent { + + @Override + public String getType() { return "mcp.headers_refresh_required"; } + + @JsonProperty("data") + private McpHeadersRefreshRequiredEventData data; + + public McpHeadersRefreshRequiredEventData getData() { return data; } + public void setData(McpHeadersRefreshRequiredEventData data) { this.data = data; } + + /** Data payload for {@link McpHeadersRefreshRequiredEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record McpHeadersRefreshRequiredEventData( + /** Unique identifier for this headers refresh request; used to respond via session.mcp.headers.handlePendingHeadersRefreshRequest() */ + @JsonProperty("requestId") String requestId, + /** Display name of the remote MCP server requesting headers */ + @JsonProperty("serverName") String serverName, + /** URL of the remote MCP server requesting headers */ + @JsonProperty("serverUrl") String serverUrl, + /** Why dynamic headers are being requested. */ + @JsonProperty("reason") McpHeadersRefreshRequiredReason reason + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java new file mode 100644 index 0000000000..86c8f8b2d6 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Why dynamic headers are being requested. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpHeadersRefreshRequiredReason { + /** The {@code startup} variant. */ + STARTUP("startup"), + /** The {@code ttl-expired} variant. */ + TTL_EXPIRED("ttl-expired"), + /** The {@code auth-failed} variant. */ + AUTH_FAILED("auth-failed"); + + private final String value; + McpHeadersRefreshRequiredReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpHeadersRefreshRequiredReason fromValue(String value) { + for (McpHeadersRefreshRequiredReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpHeadersRefreshRequiredReason value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java new file mode 100644 index 0000000000..2a6eec7063 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Reason the runtime is requesting host-provided MCP OAuth credentials + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpOauthRequestReason { + /** The {@code initial} variant. */ + INITIAL("initial"), + /** The {@code refresh} variant. */ + REFRESH("refresh"), + /** The {@code reauth} variant. */ + REAUTH("reauth"), + /** The {@code upscope} variant. */ + UPSCOPE("upscope"); + + private final String value; + McpOauthRequestReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpOauthRequestReason fromValue(String value) { + for (McpOauthRequestReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpOauthRequestReason value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java index 3f6ef8ef60..67413d382f 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java @@ -45,7 +45,9 @@ public record McpOauthRequiredEventData( /** OAuth WWW-Authenticate parameters parsed from the auth challenge, if available */ @JsonProperty("wwwAuthenticateParams") McpOauthWWWAuthenticateParams wwwAuthenticateParams, /** Raw OAuth protected-resource metadata document fetched for the MCP server, if available */ - @JsonProperty("resourceMetadata") String resourceMetadata + @JsonProperty("resourceMetadata") String resourceMetadata, + /** Why the runtime is requesting host-provided OAuth credentials. */ + @JsonProperty("reason") McpOauthRequestReason reason ) { } } diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java index 764f8b7fc8..5f42ec90c4 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java @@ -23,6 +23,8 @@ public record McpOauthRequiredStaticClientConfig( /** OAuth client ID for the server */ @JsonProperty("clientId") String clientId, + /** Optional OAuth client secret for confidential static clients, when the runtime can resolve one */ + @JsonProperty("clientSecret") String clientSecret, /** Whether this is a public OAuth client */ @JsonProperty("publicClient") Boolean publicClient, /** Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). */ diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java b/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java index faa08e1e89..3e1fdb0d10 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record McpOauthWWWAuthenticateParams( - /** Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter */ + /** Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter, if present */ @JsonProperty("resourceMetadataUrl") String resourceMetadataUrl, /** Requested OAuth scopes from the WWW-Authenticate scope parameter, if present */ @JsonProperty("scope") String scope, diff --git a/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java b/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java new file mode 100644 index 0000000000..0bc290e2df --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional experimental response budget limits. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ResponseBudgetConfig( + /** Maximum model-call iterations allowed while responding to one top-level user message. */ + @JsonProperty("maxModelIterations") Long maxModelIterations, + /** Maximum AI Credits allowed while responding to one top-level user message. */ + @JsonProperty("maxAiCredits") Double maxAiCredits +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java index f92dbf2fae..bcbb09831c 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -63,6 +63,7 @@ @JsonSubTypes.Type(value = AssistantMessageStartEvent.class, name = "assistant.message_start"), @JsonSubTypes.Type(value = AssistantMessageDeltaEvent.class, name = "assistant.message_delta"), @JsonSubTypes.Type(value = AssistantTurnEndEvent.class, name = "assistant.turn_end"), + @JsonSubTypes.Type(value = AssistantIdleEvent.class, name = "assistant.idle"), @JsonSubTypes.Type(value = AssistantUsageEvent.class, name = "assistant.usage"), @JsonSubTypes.Type(value = ModelCallFailureEvent.class, name = "model.call_failure"), @JsonSubTypes.Type(value = AbortEvent.class, name = "abort"), @@ -93,6 +94,8 @@ @JsonSubTypes.Type(value = SamplingCompletedEvent.class, name = "sampling.completed"), @JsonSubTypes.Type(value = McpOauthRequiredEvent.class, name = "mcp.oauth_required"), @JsonSubTypes.Type(value = McpOauthCompletedEvent.class, name = "mcp.oauth_completed"), + @JsonSubTypes.Type(value = McpHeadersRefreshRequiredEvent.class, name = "mcp.headers_refresh_required"), + @JsonSubTypes.Type(value = McpHeadersRefreshCompletedEvent.class, name = "mcp.headers_refresh_completed"), @JsonSubTypes.Type(value = SessionCustomNotificationEvent.class, name = "session.custom_notification"), @JsonSubTypes.Type(value = ExternalToolRequestedEvent.class, name = "external_tool.requested"), @JsonSubTypes.Type(value = ExternalToolCompletedEvent.class, name = "external_tool.completed"), @@ -161,6 +164,7 @@ public abstract sealed class SessionEvent permits AssistantMessageStartEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, + AssistantIdleEvent, AssistantUsageEvent, ModelCallFailureEvent, AbortEvent, @@ -191,6 +195,8 @@ public abstract sealed class SessionEvent permits SamplingCompletedEvent, McpOauthRequiredEvent, McpOauthCompletedEvent, + McpHeadersRefreshRequiredEvent, + McpHeadersRefreshCompletedEvent, SessionCustomNotificationEvent, ExternalToolRequestedEvent, ExternalToolCompletedEvent, diff --git a/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java index b1cfda9338..d4c2978777 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java @@ -49,6 +49,8 @@ public record SessionResumeEventData( @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Context tier currently selected at resume time; null when no tier is active */ @JsonProperty("contextTier") ContextTier contextTier, + /** Experimental response budget limits currently configured at resume time; null when no budget is active */ + @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget, /** Updated working directory and git context at resume time */ @JsonProperty("context") WorkingDirectoryContext context, /** Whether the session was already in use by another client at resume time */ diff --git a/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java index 2b36fa3e7b..6d2921ff05 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java @@ -53,6 +53,8 @@ public record SessionStartEventData( @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) */ @JsonProperty("contextTier") ContextTier contextTier, + /** Experimental response budget limits configured at session creation time, if any */ + @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget, /** Working directory and git context at session start */ @JsonProperty("context") WorkingDirectoryContext context, /** Whether the session was already in use by another client at start time */ diff --git a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java index cd4e5a137d..b119a3eb0e 100644 --- a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java @@ -40,6 +40,8 @@ public record ToolExecutionStartEventData( @JsonProperty("toolName") String toolName, /** Arguments passed to the tool */ @JsonProperty("arguments") Object arguments, + /** Shell-tool path hints derived from the command at start time for shell tools (bash/powershell/local_shell). Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. Absent for non-shell tools. */ + @JsonProperty("shellToolInfo") ToolExecutionStartShellToolInfo shellToolInfo, /** Model identifier that generated this tool call */ @JsonProperty("model") String model, /** Name of the MCP server hosting this tool, when the tool is an MCP tool */ diff --git a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java new file mode 100644 index 0000000000..88ac787fca --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Shell-aware path hints for a shell tool's command, captured at start time so consumers can snapshot a file's pre-image before the tool runs. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ToolExecutionStartShellToolInfo( + /** File paths the command may read or write, derived from the command at start time. Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. */ + @JsonProperty("possiblePaths") List possiblePaths, + /** Whether the command includes a file write redirection (e.g., > or >>). */ + @JsonProperty("hasWriteFileRedirection") Boolean hasWriteFileRedirection +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java b/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java new file mode 100644 index 0000000000..ab64a88592 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum UserMessageDelivery { + /** The {@code idle} variant. */ + IDLE("idle"), + /** The {@code steering} variant. */ + STEERING("steering"), + /** The {@code queued} variant. */ + QUEUED("queued"); + + private final String value; + UserMessageDelivery(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static UserMessageDelivery fromValue(String value) { + for (UserMessageDelivery v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown UserMessageDelivery value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java b/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java index 69f959ab65..57f64b6527 100644 --- a/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java @@ -47,6 +47,8 @@ public record UserMessageEventData( @JsonProperty("nativeDocumentPathFallbackPaths") List nativeDocumentPathFallbackPaths, /** Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user) */ @JsonProperty("source") String source, + /** How this message was delivered to the agentic loop relative to loop state (idle-start vs. steering/queued while busy). The timing axis; combine with `source` (origin) for the full picture. Used for telemetry attribution. */ + @JsonProperty("delivery") UserMessageDelivery delivery, /** The agent mode that was active when this message was sent */ @JsonProperty("agentMode") UserMessageAgentMode agentMode, /** True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. */ diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java index 94a8188f16..53ed64f4bf 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java @@ -24,6 +24,8 @@ public record ModelBilling( /** Billing cost multiplier relative to the base rate */ @JsonProperty("multiplier") Double multiplier, /** Token-level pricing information for this model */ - @JsonProperty("tokenPrices") ModelBillingTokenPrices tokenPrices + @JsonProperty("tokenPrices") ModelBillingTokenPrices tokenPrices, + /** Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. */ + @JsonProperty("discountPercent") Long discountPercent ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java b/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java new file mode 100644 index 0000000000..4cbe89b4be --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional experimental response budget limits. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ResponseBudgetConfig( + /** Maximum model-call iterations allowed while responding to one top-level user message. */ + @JsonProperty("maxModelIterations") Long maxModelIterations, + /** Maximum AI Credits allowed while responding to one top-level user message. */ + @JsonProperty("maxAiCredits") Double maxAiCredits +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java index 4246b11f82..f4603c249f 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java @@ -26,6 +26,8 @@ public final class SessionMcpApi { /** API methods for the {@code mcp.oauth} sub-namespace. */ public final SessionMcpOauthApi oauth; + /** API methods for the {@code mcp.headers} sub-namespace. */ + public final SessionMcpHeadersApi headers; /** API methods for the {@code mcp.apps} sub-namespace. */ public final SessionMcpAppsApi apps; @@ -34,6 +36,7 @@ public final class SessionMcpApi { this.caller = caller; this.sessionId = sessionId; this.oauth = new SessionMcpOauthApi(caller, sessionId); + this.headers = new SessionMcpHeadersApi(caller, sessionId); this.apps = new SessionMcpAppsApi(caller, sessionId); } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java new file mode 100644 index 0000000000..45679f83a4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code mcp.headers} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpHeadersApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionMcpHeadersApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * MCP headers refresh request id and the host response. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture handlePendingHeadersRefreshRequest(SessionMcpHeadersHandlePendingHeadersRefreshRequestParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.headers.handlePendingHeadersRefreshRequest", _p, SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java new file mode 100644 index 0000000000..77ce6f7329 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * MCP headers refresh request id and the host response. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpHeadersHandlePendingHeadersRefreshRequestParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Headers refresh request identifier from mcp.headers_refresh_required */ + @JsonProperty("requestId") String requestId, + /** Host response: supply dynamic headers or decline this refresh. */ + @JsonProperty("result") Object result +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java new file mode 100644 index 0000000000..a890713060 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the pending MCP headers refresh response was accepted. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpHeadersHandlePendingHeadersRefreshRequestResult( + /** Whether the response was accepted. False if the request was unknown, timed out, or already resolved. */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java index 4479a3175a..dac9741a4d 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java @@ -70,6 +70,8 @@ public record SessionOptionsUpdateParams( @JsonProperty("logInteractiveShells") Boolean logInteractiveShells, /** How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). */ @JsonProperty("envValueMode") OptionsUpdateEnvValueMode envValueMode, + /** Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. */ + @JsonProperty("allowAllMcpServerInstructions") Boolean allowAllMcpServerInstructions, /** Additional directories to search for skills. */ @JsonProperty("skillDirectories") List skillDirectories, /** Skill IDs that should be excluded from this session. */ @@ -127,6 +129,8 @@ public record SessionOptionsUpdateParams( /** Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. */ @JsonProperty("enableSkills") Boolean enableSkills, /** Context tier for models with tiered pricing. The session uses this to derive effective `modelCapabilitiesOverrides` so compaction, truncation, token display, and request limits honor the selected tier. */ - @JsonProperty("contextTier") OptionsUpdateContextTier contextTier + @JsonProperty("contextTier") OptionsUpdateContextTier contextTier, + /** Optional experimental response budget limits. Pass null to clear the response budget. */ + @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java index 44758350bd..d8c0c64fd0 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java @@ -39,7 +39,11 @@ public record SessionToolsUpdateSubagentSettingsParamsSubagents( /** Per-agent settings keyed by subagent agent_type */ @JsonProperty("agents") Map agents, /** Names of subagents the user has turned off; they cannot be dispatched */ - @JsonProperty("disabledSubagents") List disabledSubagents + @JsonProperty("disabledSubagents") List disabledSubagents, + /** Maximum number of subagents that can run concurrently; applies to usage-based billing users only */ + @JsonProperty("maxConcurrency") Long maxConcurrency, + /** Maximum subagent nesting depth; applies to usage-based billing users only */ + @JsonProperty("maxDepth") Long maxDepth ) { } } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java index 265f24e669..336c0eda84 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Result of invoking the slash command (text output, prompt to send to the agent, or completion). + * Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). * * @since 1.0.0 */ diff --git a/java/src/main/java/com/github/copilot/CopilotClient.java b/java/src/main/java/com/github/copilot/CopilotClient.java index 1a49941895..56a9ab4252 100644 --- a/java/src/main/java/com/github/copilot/CopilotClient.java +++ b/java/src/main/java/com/github/copilot/CopilotClient.java @@ -27,6 +27,7 @@ import com.github.copilot.generated.rpc.SessionInstalledPlugin; import com.github.copilot.generated.rpc.ConnectParams; import com.github.copilot.generated.rpc.ServerRpc; +import com.github.copilot.generated.rpc.SessionEventLogRegisterInterestParams; import com.github.copilot.rpc.DeleteSessionResponse; import com.github.copilot.rpc.GetAuthStatusResponse; import com.github.copilot.rpc.GetLastSessionIdResponse; @@ -638,14 +639,19 @@ public CompletableFuture createSession(SessionConfig config) { ? preRegisteredSessionHolder[0] : initializeSession.apply(returnedId); registeredIdHolder[0] = returnedId; + CompletableFuture interest = config.getOnMcpAuthRequest() != null + ? session.getRpc().eventLog.registerInterest( + new SessionEventLogRegisterInterestParams(returnedId, "mcp.oauth_required")) + : CompletableFuture.completedFuture(null); session.setWorkspacePath(response.workspacePath()); session.setCapabilities(response.capabilities()); session.setOpenCanvases(response.openCanvases()); - return updateSessionOptionsForMode(session, config.getSkipCustomInstructions().orElse(null), + return interest.thenCompose(unusedResult -> updateSessionOptionsForMode(session, + config.getSkipCustomInstructions().orElse(null), config.getCustomAgentsLocalOnly().orElse(null), config.getCoauthorEnabled().orElse(null), - config.getManageScheduleEnabled().orElse(null)).thenApply(v -> { + config.getManageScheduleEnabled().orElse(null))).thenApply(v -> { LoggingHelpers.logTiming(LOG, Level.FINE, "CopilotClient.createSession complete. Elapsed={Elapsed}, SessionId=" + session.getSessionId(), @@ -714,6 +720,10 @@ public CompletableFuture resumeSession(String sessionId, ResumeS if (extracted.transformCallbacks() != null) { session.registerTransformCallbacks(extracted.transformCallbacks()); } + CompletableFuture interest = config.getOnMcpAuthRequest() != null + ? session.getRpc().eventLog.registerInterest( + new SessionEventLogRegisterInterestParams(sessionId, "mcp.oauth_required")) + : CompletableFuture.completedFuture(null); var request = SessionRequestBuilder.buildResumeRequest(sessionId, config); if (extracted.wireSystemMessage() != config.getSystemMessage()) { @@ -760,7 +770,8 @@ public CompletableFuture resumeSession(String sessionId, ResumeS } long rpcNanos = System.nanoTime(); - return connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class) + return interest.thenCompose( + unusedResult -> connection.rpc.invoke("session.resume", request, ResumeSessionResponse.class)) .thenCompose(response -> { LoggingHelpers.logTiming(LOG, Level.FINE, "CopilotClient.resumeSession session resume request completed. Elapsed={Elapsed}, SessionId=" @@ -886,6 +897,7 @@ CompletableFuture updateSessionOptionsForMode(CopilotSession session, Bool null, // sandboxConfig null, // logInteractiveShells null, // envValueMode + null, // allowAllMcpServerInstructions null, // skillDirectories null, // disabledSkills null, // enableOnDemandInstructionDiscovery @@ -914,7 +926,8 @@ CompletableFuture updateSessionOptionsForMode(CopilotSession session, Bool null, // enableHostGitOperations null, // enableSessionStore null, // enableSkills - null // contextTier + null, // contextTier + null // responseBudget ); return session.getRpc().options.update(params).thenCompose(result -> { diff --git a/java/src/main/java/com/github/copilot/CopilotSession.java b/java/src/main/java/com/github/copilot/CopilotSession.java index 90f76b6df5..d33f3e197e 100644 --- a/java/src/main/java/com/github/copilot/CopilotSession.java +++ b/java/src/main/java/com/github/copilot/CopilotSession.java @@ -33,6 +33,7 @@ import com.github.copilot.generated.rpc.SessionCommandsHandlePendingCommandParams; import com.github.copilot.generated.rpc.SessionLogParams; import com.github.copilot.generated.rpc.SessionLogLevel; +import com.github.copilot.generated.rpc.SessionMcpOauthHandlePendingRequestParams; import com.github.copilot.generated.rpc.ModelCapabilitiesOverride; import com.github.copilot.generated.rpc.ModelCapabilitiesOverrideLimits; import com.github.copilot.generated.rpc.ModelCapabilitiesOverrideSupports; @@ -49,6 +50,7 @@ import com.github.copilot.generated.CommandExecuteEvent; import com.github.copilot.generated.ElicitationRequestedEvent; import com.github.copilot.generated.ExternalToolRequestedEvent; +import com.github.copilot.generated.McpOauthRequiredEvent; import com.github.copilot.generated.PermissionRequestedEvent; import com.github.copilot.generated.SessionCanvasClosedEvent; import com.github.copilot.generated.SessionCanvasOpenedEvent; @@ -79,6 +81,9 @@ import com.github.copilot.rpc.HookInvocation; import com.github.copilot.rpc.InputOptions; import com.github.copilot.rpc.MessageOptions; +import com.github.copilot.rpc.McpAuthHandler; +import com.github.copilot.rpc.McpAuthRequest; +import com.github.copilot.rpc.McpAuthResult; import com.github.copilot.rpc.PermissionHandler; import com.github.copilot.rpc.PermissionInvocation; import com.github.copilot.rpc.PermissionRequest; @@ -171,6 +176,7 @@ public final class CopilotSession implements AutoCloseable { private final Map commandHandlers = new ConcurrentHashMap<>(); private final Map bearerTokenProviders = new ConcurrentHashMap<>(); private final AtomicReference permissionHandler = new AtomicReference<>(); + private final AtomicReference mcpAuthHandler = new AtomicReference<>(); private final AtomicReference userInputHandler = new AtomicReference<>(); private final AtomicReference elicitationHandler = new AtomicReference<>(); private final AtomicReference exitPlanModeHandler = new AtomicReference<>(); @@ -839,6 +845,21 @@ private void handleBroadcastEventAsync(SessionEvent event) { } executePermissionAndRespondAsync(data.requestId(), MAPPER.convertValue(data.permissionRequest(), PermissionRequest.class), handler); + } else if (event instanceof McpOauthRequiredEvent authEvent) { + var data = authEvent.getData(); + if (data == null || data.requestId() == null) { + return; + } + McpAuthHandler handler = mcpAuthHandler.get(); + if (handler == null) { + LOG.warning(() -> "Received MCP OAuth request without a registered MCP auth handler. SessionId=" + + sessionId + ", RequestId=" + data.requestId()); + return; + } + executeMcpAuthAndRespondAsync( + new McpAuthRequest(sessionId, data.requestId(), data.serverName(), data.serverUrl(), data.reason(), + data.wwwAuthenticateParams(), data.resourceMetadata(), data.staticClientConfig()), + handler); } else if (event instanceof CommandExecuteEvent cmdEvent) { var data = cmdEvent.getData(); if (data == null || data.requestId() == null || data.commandName() == null) { @@ -1006,6 +1027,57 @@ private void executePermissionAndRespondAsync(String requestId, PermissionReques } } + private void executeMcpAuthAndRespondAsync(McpAuthRequest request, McpAuthHandler handler) { + Runnable task = () -> { + try { + handler.handle(request).thenAccept(result -> sendMcpAuthResponse(request.requestId(), result)) + .exceptionally(ex -> { + sendMcpAuthResponse(request.requestId(), McpAuthResult.cancelled()); + return null; + }); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error executing MCP auth handler for requestId=" + request.requestId(), e); + sendMcpAuthResponse(request.requestId(), McpAuthResult.cancelled()); + } + }; + try { + if (executor != null) { + CompletableFuture.runAsync(task, executor); + } else { + CompletableFuture.runAsync(task); + } + } catch (RejectedExecutionException e) { + LOG.log(Level.WARNING, + "Executor rejected MCP auth task for requestId=" + request.requestId() + "; running inline", e); + task.run(); + } + } + + private void sendMcpAuthResponse(String requestId, McpAuthResult result) { + try { + Object response; + if (result == null || result.isCancelled() || result.token() == null) { + response = Map.of("kind", "cancelled"); + } else { + var token = result.token(); + var tokenResponse = new java.util.HashMap(); + tokenResponse.put("kind", "token"); + tokenResponse.put("accessToken", token.accessToken()); + if (token.tokenType() != null) { + tokenResponse.put("tokenType", token.tokenType()); + } + if (token.expiresIn() != null) { + tokenResponse.put("expiresIn", token.expiresIn()); + } + response = tokenResponse; + } + getRpc().mcp.oauth.handlePendingRequest( + new SessionMcpOauthHandlePendingRequestParams(sessionId, requestId, response)); + } catch (Exception e) { + LOG.log(Level.WARNING, "Error sending MCP auth response for requestId=" + requestId, e); + } + } + /** * Registers custom tool handlers for this session. *

@@ -1269,6 +1341,10 @@ void registerPermissionHandler(PermissionHandler handler) { permissionHandler.set(handler); } + void registerMcpAuthHandler(McpAuthHandler handler) { + mcpAuthHandler.set(handler); + } + /** * Handles a permission request from the Copilot CLI. *

diff --git a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java index 6000bdef82..8a4b016e1b 100644 --- a/java/src/main/java/com/github/copilot/SessionRequestBuilder.java +++ b/java/src/main/java/com/github/copilot/SessionRequestBuilder.java @@ -323,6 +323,9 @@ static void configureSession(CopilotSession session, SessionConfig config) { if (config.getOnPermissionRequest() != null) { session.registerPermissionHandler(config.getOnPermissionRequest()); } + if (config.getOnMcpAuthRequest() != null) { + session.registerMcpAuthHandler(config.getOnMcpAuthRequest()); + } if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); } @@ -370,6 +373,9 @@ static void configureSession(CopilotSession session, ResumeSessionConfig config) if (config.getOnPermissionRequest() != null) { session.registerPermissionHandler(config.getOnPermissionRequest()); } + if (config.getOnMcpAuthRequest() != null) { + session.registerMcpAuthHandler(config.getOnMcpAuthRequest()); + } if (config.getOnUserInputRequest() != null) { session.registerUserInputHandler(config.getOnUserInputRequest()); } diff --git a/java/src/main/java/com/github/copilot/rpc/McpAuthHandler.java b/java/src/main/java/com/github/copilot/rpc/McpAuthHandler.java new file mode 100644 index 0000000000..e92ef1dd46 --- /dev/null +++ b/java/src/main/java/com/github/copilot/rpc/McpAuthHandler.java @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import java.util.concurrent.CompletableFuture; + +/** + * Handles MCP OAuth requests from the runtime. + * + * @since 1.0.0 + */ +@FunctionalInterface +public interface McpAuthHandler { + /** + * Handles an MCP OAuth request. + * + * @param request + * the MCP OAuth request context + * @return a future resolving to token data or cancellation + */ + CompletableFuture handle(McpAuthRequest request); +} diff --git a/java/src/main/java/com/github/copilot/rpc/McpAuthRequest.java b/java/src/main/java/com/github/copilot/rpc/McpAuthRequest.java new file mode 100644 index 0000000000..db8aa6043b --- /dev/null +++ b/java/src/main/java/com/github/copilot/rpc/McpAuthRequest.java @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +import com.github.copilot.generated.McpOauthRequiredStaticClientConfig; +import com.github.copilot.generated.McpOauthRequestReason; +import com.github.copilot.generated.McpOauthWWWAuthenticateParams; + +/** + * MCP OAuth request that the SDK host can satisfy with a host-acquired token. + * + * @since 1.0.0 + */ +public record McpAuthRequest(String sessionId, String requestId, String serverName, String serverUrl, + McpOauthRequestReason reason, McpOauthWWWAuthenticateParams wwwAuthenticateParams, String resourceMetadata, + McpOauthRequiredStaticClientConfig staticClientConfig) { +} diff --git a/java/src/main/java/com/github/copilot/rpc/McpAuthResult.java b/java/src/main/java/com/github/copilot/rpc/McpAuthResult.java new file mode 100644 index 0000000000..6b7fda34f9 --- /dev/null +++ b/java/src/main/java/com/github/copilot/rpc/McpAuthResult.java @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +/** + * Result returned by an MCP auth request handler. + * + * @since 1.0.0 + */ +public record McpAuthResult(boolean isCancelled, McpAuthToken token) { + /** + * Creates a token result. + * + * @param token + * the host-provided OAuth token data + * @return token result + */ + public static McpAuthResult token(McpAuthToken token) { + return new McpAuthResult(false, token); + } + + /** + * Creates a cancellation result. + * + * @return cancellation result + */ + public static McpAuthResult cancelled() { + return new McpAuthResult(true, null); + } +} diff --git a/java/src/main/java/com/github/copilot/rpc/McpAuthToken.java b/java/src/main/java/com/github/copilot/rpc/McpAuthToken.java new file mode 100644 index 0000000000..3cf6748fbf --- /dev/null +++ b/java/src/main/java/com/github/copilot/rpc/McpAuthToken.java @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.rpc; + +/** + * Host-provided OAuth token data for a pending MCP OAuth request. + * + * @since 1.0.0 + */ +public record McpAuthToken(String accessToken, String tokenType, Long expiresIn) { +} diff --git a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java index e3e79eab01..48e333f05b 100644 --- a/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java @@ -60,6 +60,7 @@ public class ResumeSessionConfig { private String contextTier; private ModelCapabilitiesOverride modelCapabilities; private PermissionHandler onPermissionRequest; + private McpAuthHandler onMcpAuthRequest; private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; @@ -635,6 +636,28 @@ public ResumeSessionConfig setOnPermissionRequest(PermissionHandler onPermission return this; } + /** + * Gets the MCP OAuth request handler. + * + * @return the handler, or {@code null} if not set + */ + @JsonIgnore + public McpAuthHandler getOnMcpAuthRequest() { + return onMcpAuthRequest; + } + + /** + * Sets the MCP OAuth request handler. + * + * @param onMcpAuthRequest + * the handler + * @return this config instance for method chaining + */ + public ResumeSessionConfig setOnMcpAuthRequest(McpAuthHandler onMcpAuthRequest) { + this.onMcpAuthRequest = onMcpAuthRequest; + return this; + } + /** * Gets the user input request handler. * @@ -1697,6 +1720,7 @@ public ResumeSessionConfig clone() { copy.onEvent = this.onEvent; copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; + copy.onMcpAuthRequest = this.onMcpAuthRequest; copy.onExitPlanMode = this.onExitPlanMode; copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.enableMcpApps = this.enableMcpApps; diff --git a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java index 38b357e7e3..e5e0e629e1 100644 --- a/java/src/main/java/com/github/copilot/rpc/SessionConfig.java +++ b/java/src/main/java/com/github/copilot/rpc/SessionConfig.java @@ -60,6 +60,7 @@ public class SessionConfig { private Boolean coauthorEnabled; private Boolean manageScheduleEnabled; private PermissionHandler onPermissionRequest; + private McpAuthHandler onMcpAuthRequest; private UserInputHandler onUserInputRequest; private SessionHooks hooks; private String workingDirectory; @@ -678,6 +679,31 @@ public SessionConfig setOnPermissionRequest(PermissionHandler onPermissionReques return this; } + /** + * Gets the MCP OAuth request handler. + * + * @return the handler, or {@code null} if not set + */ + @JsonIgnore + public McpAuthHandler getOnMcpAuthRequest() { + return onMcpAuthRequest; + } + + /** + * Sets the MCP OAuth request handler. + *

+ * When provided, the SDK can satisfy MCP server OAuth requests with + * host-provided token data or cancellation. + * + * @param onMcpAuthRequest + * the handler + * @return this config instance for method chaining + */ + public SessionConfig setOnMcpAuthRequest(McpAuthHandler onMcpAuthRequest) { + this.onMcpAuthRequest = onMcpAuthRequest; + return this; + } + /** * Gets the user input request handler. * @@ -1829,6 +1855,7 @@ public SessionConfig clone() { copy.onEvent = this.onEvent; copy.commands = this.commands != null ? new ArrayList<>(this.commands) : null; copy.onElicitationRequest = this.onElicitationRequest; + copy.onMcpAuthRequest = this.onMcpAuthRequest; copy.onExitPlanMode = this.onExitPlanMode; copy.onAutoModeSwitch = this.onAutoModeSwitch; copy.enableMcpApps = this.enableMcpApps; diff --git a/java/src/test/java/com/github/copilot/E2ETestContext.java b/java/src/test/java/com/github/copilot/E2ETestContext.java index 4089e10ff7..db008acbc1 100644 --- a/java/src/test/java/com/github/copilot/E2ETestContext.java +++ b/java/src/test/java/com/github/copilot/E2ETestContext.java @@ -288,6 +288,8 @@ public Map getEnvironment() { env.put("GH_CONFIG_DIR", homeDir.toString()); env.put("XDG_CONFIG_HOME", homeDir.toString()); env.put("XDG_STATE_HOME", homeDir.toString()); + env.put("COPILOT_MCP_APPS", "true"); + env.put("MCP_APPS", "true"); // Configure CONNECT proxy for HTTPS interception if available String connectUrl = proxy.getConnectProxyUrl(); @@ -438,7 +440,15 @@ private static Path findRepoRoot() throws IOException { } private static String getCliPath(Path repoRoot) throws IOException { - // Try environment variable first (explicit override) + Path localRuntimeCliPath = Paths.get( + "/Users/roji/.copilot/repos/copilot-worktrees/copilot-agent-runtime/roji-symmetrical-dollop/dist-cli/index.js"); + if (Files.exists(localRuntimeCliPath)) { + return localRuntimeCliPath.toString(); + } + + // Try environment variable after the temporary local runtime override. Maven + // injects COPILOT_CLI_PATH for tests, so checking it first would bypass the + // sibling runtime branch this E2E suite is validating against. String envPath = System.getenv("COPILOT_CLI_PATH"); if (envPath != null && !envPath.isEmpty()) { return envPath; diff --git a/java/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java b/java/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java new file mode 100644 index 0000000000..215eb18f14 --- /dev/null +++ b/java/src/test/java/com/github/copilot/McpAuthInterestRegistrationTest.java @@ -0,0 +1,287 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.copilot.generated.McpOauthRequiredEvent; +import com.github.copilot.rpc.CloudSessionOptions; +import com.github.copilot.rpc.CloudSessionRepository; +import com.github.copilot.rpc.CopilotClientOptions; +import com.github.copilot.rpc.McpAuthResult; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.ResumeSessionConfig; +import com.github.copilot.rpc.SessionConfig; + +class McpAuthInterestRegistrationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void mcpOauthRequiredEventExposesOptionalResourceMetadata() throws Exception { + var data = MAPPER.readValue(""" + { + "requestId": "oauth-request", + "reason": "initial", + "serverName": "oauth-server", + "serverUrl": "https://example.com/mcp", + "wwwAuthenticateParams": { + "resourceMetadataUrl": "https://example.com/.well-known/oauth-protected-resource" + }, + "resourceMetadata": "{\\"resource\\":\\"https://example.com/mcp\\"}", + "staticClientConfig": { + "clientId": "static-client", + "clientSecret": "static-secret", + "grantType": "client_credentials", + "publicClient": false + } + } + """, McpOauthRequiredEvent.McpOauthRequiredEventData.class); + + assertEquals("{\"resource\":\"https://example.com/mcp\"}", data.resourceMetadata()); + assertNotNull(data.wwwAuthenticateParams()); + assertNotNull(data.staticClientConfig()); + assertEquals("static-secret", data.staticClientConfig().clientSecret()); + + var withoutMetadata = MAPPER.readValue(""" + { + "requestId": "oauth-request", + "reason": "initial", + "serverName": "oauth-server", + "serverUrl": "https://example.com/mcp" + } + """, McpOauthRequiredEvent.McpOauthRequiredEventData.class); + + assertNull(withoutMetadata.resourceMetadata()); + assertNull(withoutMetadata.wwwAuthenticateParams()); + } + + @Test + void createSessionRegistersMcpAuthInterestOnlyWhenHandlerConfigured() throws Exception { + try (var server = new RecordingRuntime(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()))) { + try (var session = client.createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setOnEvent(event -> { + })).get()) { + assertNotNull(session); + } + + assertNoMcpAuthInterest(server.requests()); + assertTrue(server.requests().stream().anyMatch(request -> "session.create".equals(request.method()) + && request.params().path("requestPermission").asBoolean())); + + server.clearRequests(); + + try (var session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnMcpAuthRequest(request -> java.util.concurrent.CompletableFuture + .completedFuture(McpAuthResult.cancelled()))) + .get()) { + assertNotNull(session); + } + + List requests = server.requests(); + assertEquals("session.create", requests.get(0).method()); + assertEquals("session.eventLog.registerInterest", requests.get(1).method()); + assertEquals("mcp.oauth_required", requests.get(1).params().path("eventType").asText()); + } + } + + @Test + void cloudCreateSessionRegistersMcpAuthInterestAfterCreateOnlyWhenHandlerConfigured() throws Exception { + try (var server = new RecordingRuntime(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()))) { + var cloud = new CloudSessionOptions().setRepository( + new CloudSessionRepository().setOwner("github").setName("copilot-sdk").setBranch("main")); + + try (var session = client + .createSession( + new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setCloud(cloud)) + .get()) { + assertNotNull(session); + } + + assertNoMcpAuthInterest(server.requests()); + server.clearRequests(); + + try (var session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setCloud(cloud).setOnMcpAuthRequest(request -> java.util.concurrent.CompletableFuture + .completedFuture(McpAuthResult.cancelled()))) + .get()) { + assertNotNull(session); + } + + List requests = server.requests(); + assertEquals("session.create", requests.get(0).method()); + assertEquals("session.eventLog.registerInterest", requests.get(1).method()); + assertEquals("mcp.oauth_required", requests.get(1).params().path("eventType").asText()); + } + } + + @Test + void resumeSessionRegistersMcpAuthInterestOnlyWhenHandlerConfigured() throws Exception { + try (var server = new RecordingRuntime(); + var client = new CopilotClient(new CopilotClientOptions().setCliUrl(server.url()))) { + try (var session = client.resumeSession("session-without-auth", new ResumeSessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setOnEvent(event -> { + })).get()) { + assertNotNull(session); + } + + assertNoMcpAuthInterest(server.requests()); + assertTrue(server.requests().stream().anyMatch(request -> "session.resume".equals(request.method()) + && request.params().path("requestPermission").asBoolean())); + + server.clearRequests(); + + try (var session = client.resumeSession("session-with-auth", + new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setOnMcpAuthRequest(request -> java.util.concurrent.CompletableFuture + .completedFuture(McpAuthResult.cancelled()))) + .get()) { + assertNotNull(session); + } + + List requests = server.requests(); + assertEquals("session.eventLog.registerInterest", requests.get(0).method()); + assertEquals("mcp.oauth_required", requests.get(0).params().path("eventType").asText()); + assertEquals("session.resume", requests.get(1).method()); + } + } + + private static void assertNoMcpAuthInterest(List requests) { + assertFalse(requests.stream().anyMatch(request -> "session.eventLog.registerInterest".equals(request.method()) + && "mcp.oauth_required".equals(request.params().path("eventType").asText()))); + } + + private record RpcRequest(String method, JsonNode params) { + } + + private static final class RecordingRuntime implements AutoCloseable { + private final ServerSocket listener; + private final Thread thread; + private final List requests = new CopyOnWriteArrayList<>(); + private volatile boolean running = true; + + RecordingRuntime() throws Exception { + listener = new ServerSocket(0); + thread = new Thread(this::run, "mcp-auth-interest-test-runtime"); + thread.setDaemon(true); + thread.start(); + } + + String url() { + return "127.0.0.1:" + listener.getLocalPort(); + } + + List requests() { + return List.copyOf(requests); + } + + void clearRequests() { + requests.clear(); + } + + @Override + public void close() throws Exception { + running = false; + listener.close(); + thread.join(2000); + } + + private void run() { + try (Socket socket = listener.accept()) { + var in = socket.getInputStream(); + var out = socket.getOutputStream(); + while (running) { + JsonNode message = readMessage(in); + if (message == null) { + return; + } + String method = message.path("method").asText(); + requests.add(new RpcRequest(method, message.path("params").deepCopy())); + sendResponse(out, message.path("id").asLong(), resultFor(method, message.path("params"))); + } + } catch (Exception ex) { + if (running) { + throw new RuntimeException(ex); + } + } + } + + private static JsonNode resultFor(String method, JsonNode params) { + ObjectNode result = MAPPER.createObjectNode(); + switch (method) { + case "connect" -> { + result.put("ok", true); + result.put("protocolVersion", 3); + result.put("version", "test"); + } + case "session.create", "session.resume" -> { + String sessionId = params.path("sessionId").asText("server-assigned-session"); + if (sessionId.isEmpty()) { + sessionId = "server-assigned-session"; + } + result.put("sessionId", sessionId); + result.putNull("workspacePath"); + result.putNull("capabilities"); + } + case "session.eventLog.registerInterest" -> result.put("id", "interest-1"); + case "session.options.update" -> result.put("success", true); + case "session.skills.reload", "session.destroy" -> { + } + default -> throw new IllegalStateException("Unexpected RPC method " + method); + } + return result; + } + + private static JsonNode readMessage(java.io.InputStream in) throws Exception { + StringBuilder header = new StringBuilder(); + int b; + while ((b = in.read()) != -1) { + header.append((char) b); + if (header.toString().endsWith("\r\n\r\n")) { + break; + } + } + if (b == -1) { + return null; + } + int contentLength = 0; + for (String line : header.toString().split("\r\n")) { + int colon = line.indexOf(':'); + if (colon > 0 && "Content-Length".equals(line.substring(0, colon))) { + contentLength = Integer.parseInt(line.substring(colon + 1).trim()); + } + } + byte[] body = in.readNBytes(contentLength); + return MAPPER.readTree(body); + } + + private static void sendResponse(OutputStream out, long id, JsonNode result) throws Exception { + ObjectNode response = MAPPER.createObjectNode(); + response.put("jsonrpc", "2.0"); + response.put("id", id); + response.set("result", result); + byte[] body = MAPPER.writeValueAsBytes(response); + out.write(("Content-Length: " + body.length + "\r\n\r\n").getBytes(StandardCharsets.UTF_8)); + out.write(body); + out.flush(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/McpOAuthE2ETest.java b/java/src/test/java/com/github/copilot/McpOAuthE2ETest.java new file mode 100644 index 0000000000..9b7bc156d1 --- /dev/null +++ b/java/src/test/java/com/github/copilot/McpOAuthE2ETest.java @@ -0,0 +1,291 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.copilot.generated.McpOauthRequestReason; +import com.github.copilot.generated.rpc.SessionMcpAppsCallToolParams; +import com.github.copilot.generated.rpc.McpServerStatus; +import com.github.copilot.generated.rpc.SessionMcpListToolsParams; +import com.github.copilot.rpc.McpAuthResult; +import com.github.copilot.rpc.McpAuthToken; +import com.github.copilot.rpc.McpHttpServerConfig; +import com.github.copilot.rpc.PermissionHandler; +import com.github.copilot.rpc.SessionConfig; + +public class McpOAuthE2ETest { + private static final String EXPECTED_TOKEN = "sdk-host-token"; + private static final String REFRESH_TOKEN = EXPECTED_TOKEN + "-refresh"; + private static final String UPSCOPE_TOKEN = EXPECTED_TOKEN + "-upscope"; + private static final String REAUTH_TOKEN = EXPECTED_TOKEN + "-reauth"; + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private static E2ETestContext ctx; + + @BeforeAll + static void setup() throws Exception { + ctx = E2ETestContext.create(); + } + + @AfterAll + static void teardown() throws Exception { + if (ctx != null) { + ctx.close(); + } + } + + @Test + void testShouldSatisfyMcpOauthUsingHostProvidedToken() throws Exception { + try (var oauthServer = OAuthMcpServer.start(ctx.getRepoRoot())) { + var serverName = "oauth-protected-mcp"; + var observedRequest = new java.util.concurrent.atomic.AtomicReference(); + + try (var client = ctx.createClient(); + var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setOnMcpAuthRequest(request -> { + observedRequest.set(request); + return java.util.concurrent.CompletableFuture.completedFuture( + McpAuthResult.token(new McpAuthToken(EXPECTED_TOKEN, "Bearer", 3600L))); + }).setMcpServers(Map.of(serverName, new McpHttpServerConfig() + .setUrl(oauthServer.url() + "/mcp").setTools(List.of("*"))))) + .get()) { + waitForMcpServerStatus(session, serverName, McpServerStatus.CONNECTED, observedRequest); + var tools = session.getRpc().mcp.listTools(new SessionMcpListToolsParams(null, serverName)).get(30, + TimeUnit.SECONDS); + assertTrue(tools.tools().stream().anyMatch(tool -> "whoami".equals(tool.name()))); + } + + var request = observedRequest.get(); + assertNotNull(request, "MCP auth handler should be invoked"); + assertEquals(serverName, request.serverName()); + assertEquals(oauthServer.url() + "/mcp", request.serverUrl()); + assertEquals(McpOauthRequestReason.INITIAL, request.reason()); + assertNotNull(request.wwwAuthenticateParams()); + assertEquals(oauthServer.url() + "/.well-known/oauth-protected-resource", + request.wwwAuthenticateParams().resourceMetadataUrl()); + assertEquals("mcp.read", request.wwwAuthenticateParams().scope()); + assertEquals("invalid_token", request.wwwAuthenticateParams().error()); + assertEquals(oauthServer.url() + "/mcp", + MAPPER.readTree(request.resourceMetadata()).path("resource").asText()); + + var requests = oauthServer.requests(); + assertTrue(requests.stream().anyMatch(record -> record.authorization() == null)); + assertTrue( + requests.stream().anyMatch(record -> ("Bearer " + EXPECTED_TOKEN).equals(record.authorization()))); + } + } + + @Test + void testShouldRequestReplacementTokensAcrossMcpOauthLifecycle() throws Exception { + try (var oauthServer = OAuthMcpServer.start(ctx.getRepoRoot())) { + var serverName = "oauth-lifecycle-mcp"; + var observedReasons = new CopyOnWriteArrayList(); + var refreshCount = new java.util.concurrent.atomic.AtomicInteger(); + + try (var client = ctx.createClient(); + var session = client.createSession(new SessionConfig().setEnableMcpApps(true) + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setOnMcpAuthRequest(request -> { + observedReasons.add(request.reason()); + var result = switch (request.reason()) { + case REFRESH -> { + assertNotNull(request.wwwAuthenticateParams()); + assertNull(request.wwwAuthenticateParams().resourceMetadataUrl()); + assertEquals("invalid_token", request.wwwAuthenticateParams().error()); + if (refreshCount.incrementAndGet() > 1) { + yield McpAuthResult.cancelled(); + } + yield McpAuthResult.token(new McpAuthToken(REFRESH_TOKEN, null, null)); + } + case UPSCOPE -> { + assertNotNull(request.wwwAuthenticateParams()); + assertEquals(oauthServer.url() + "/.well-known/oauth-protected-resource", + request.wwwAuthenticateParams().resourceMetadataUrl()); + assertEquals("mcp.write", request.wwwAuthenticateParams().scope()); + assertEquals("insufficient_scope", request.wwwAuthenticateParams().error()); + yield McpAuthResult.token(new McpAuthToken(UPSCOPE_TOKEN, null, null)); + } + case REAUTH -> McpAuthResult.token(new McpAuthToken(REAUTH_TOKEN, null, null)); + default -> McpAuthResult.token(new McpAuthToken(EXPECTED_TOKEN, null, null)); + }; + return java.util.concurrent.CompletableFuture.completedFuture(result); + }).setMcpServers(Map.of(serverName, new McpHttpServerConfig() + .setUrl(oauthServer.url() + "/mcp").setTools(List.of("*"))))) + .get()) { + waitForMcpServerStatus(session, serverName, McpServerStatus.CONNECTED, + new java.util.concurrent.atomic.AtomicReference<>()); + callWhoami(session, serverName, "refresh"); + callWhoami(session, serverName, "upscope"); + callWhoami(session, serverName, "reauth"); + } + + assertEquals(List.of(McpOauthRequestReason.INITIAL, McpOauthRequestReason.REFRESH, + McpOauthRequestReason.UPSCOPE, McpOauthRequestReason.REFRESH, McpOauthRequestReason.REAUTH), + observedReasons); + + var requests = oauthServer.requests(); + assertTrue( + requests.stream().anyMatch(record -> ("Bearer " + REFRESH_TOKEN).equals(record.authorization()))); + assertTrue( + requests.stream().anyMatch(record -> ("Bearer " + UPSCOPE_TOKEN).equals(record.authorization()))); + assertTrue(requests.stream().anyMatch(record -> ("Bearer " + REAUTH_TOKEN).equals(record.authorization()))); + } + } + + @Test + void testShouldCancelPendingMcpOauthRequest() throws Exception { + try (var oauthServer = OAuthMcpServer.start(ctx.getRepoRoot())) { + var serverName = "oauth-cancelled-mcp"; + var observedRequest = new java.util.concurrent.atomic.AtomicReference(); + + try (var client = ctx.createClient(); + var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setOnMcpAuthRequest(request -> { + observedRequest.set(request); + return java.util.concurrent.CompletableFuture + .completedFuture(McpAuthResult.cancelled()); + }).setMcpServers(Map.of(serverName, new McpHttpServerConfig() + .setUrl(oauthServer.url() + "/mcp").setTools(List.of("*"))))) + .get()) { + waitForMcpServerStatus(session, serverName, McpServerStatus.FAILED, observedRequest); + } + + var request = observedRequest.get(); + assertNotNull(request, "MCP auth handler should be invoked"); + assertEquals(serverName, request.serverName()); + assertEquals(McpOauthRequestReason.INITIAL, request.reason()); + } + } + + private static void callWhoami(CopilotSession session, String serverName, String scenario) throws Exception { + var result = session.getRpc().mcp.apps.callTool( + new SessionMcpAppsCallToolParams(null, serverName, "whoami", Map.of("scenario", scenario), serverName)) + .get(30, TimeUnit.SECONDS); + var content = result.path("content"); + assertEquals(1, content.size()); + assertEquals("oauth-test-user", content.get(0).path("text").asText()); + } + + private static void waitForMcpServerStatus(CopilotSession session, String serverName, McpServerStatus status, + java.util.concurrent.atomic.AtomicReference observedRequest) + throws Exception { + var deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(60); + var lastStatus = ""; + while (System.nanoTime() < deadline) { + var result = session.getRpc().mcp.list().get(5, TimeUnit.SECONDS); + var server = result.servers().stream().filter(candidate -> serverName.equals(candidate.name())).findFirst(); + if (server.isPresent()) { + lastStatus = String.valueOf(server.get().status()); + } + if (server.isPresent() && status.equals(server.get().status())) { + return; + } + Thread.sleep(200); + } + fail(serverName + " did not reach " + status + "; last status was " + lastStatus + "; auth handler invoked=" + + (observedRequest.get() != null)); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private record OAuthMcpRequest(String authorization) { + } + + private record OAuthMcpServer(Process process, String url) implements AutoCloseable { + static OAuthMcpServer start(Path repoRoot) throws Exception { + var script = repoRoot.resolve("test").resolve("harness").resolve("test-mcp-oauth-server.mjs"); + var processBuilder = new ProcessBuilder(resolveExecutable("node"), script.toString()); + processBuilder.environment().put("EXPECTED_TOKEN", EXPECTED_TOKEN); + var process = processBuilder.start(); + var stderr = new StringBuilder(); + Thread stderrThread = new Thread(() -> { + try (var reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + reader.lines().forEach(stderr::append); + } catch (IOException ex) { + stderr.append(ex.getMessage()); + } + }); + stderrThread.setDaemon(true); + stderrThread.start(); + try (var reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + var deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10); + while (System.nanoTime() < deadline) { + if (reader.ready()) { + var line = reader.readLine(); + if (line != null && line.startsWith("Listening: ")) { + return new OAuthMcpServer(process, line.substring("Listening: ".length())); + } + } + Thread.sleep(50); + } + } + process.destroyForcibly(); + throw new AssertionError("Timed out waiting for OAuth MCP server: " + stderr); + } + + List requests() throws Exception { + var client = HttpClient.newHttpClient(); + var response = client.send(HttpRequest.newBuilder(URI.create(url + "/__requests")) + .timeout(Duration.ofSeconds(10)).GET().build(), HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode()); + return MAPPER.readValue(response.body(), new TypeReference>() { + }); + } + + private static String resolveExecutable(String executable) { + var path = System.getenv("PATH"); + if (path == null || path.isBlank()) { + throw new IllegalStateException("PATH is not configured; cannot find " + executable); + } + + var extensions = isWindows() + ? System.getenv().getOrDefault("PATHEXT", ".COM;.EXE;.BAT;.CMD").split(";") + : new String[]{""}; + for (var directory : path.split(java.util.regex.Pattern.quote(File.pathSeparator))) { + if (directory.isBlank()) { + continue; + } + for (var extension : extensions) { + var candidate = Path.of(directory).resolve(executable + extension).toAbsolutePath().normalize(); + if (Files.isRegularFile(candidate) && Files.isExecutable(candidate)) { + return candidate.toString(); + } + } + } + throw new IllegalStateException("Could not find " + executable + " on PATH."); + } + + private static boolean isWindows() { + return System.getProperty("os.name", "").toLowerCase(java.util.Locale.ROOT).contains("win"); + } + + @Override + public void close() { + process.destroyForcibly(); + } + } +} diff --git a/java/src/test/java/com/github/copilot/SessionEventHandlingTest.java b/java/src/test/java/com/github/copilot/SessionEventHandlingTest.java index 3ca56b817d..6e80b7593c 100644 --- a/java/src/test/java/com/github/copilot/SessionEventHandlingTest.java +++ b/java/src/test/java/com/github/copilot/SessionEventHandlingTest.java @@ -180,7 +180,7 @@ void testHandlerReceivesCorrectEventData() { SessionStartEvent startEvent = createSessionStartEvent(); startEvent.setData(new SessionStartEvent.SessionStartEventData("my-session-123", null, null, null, null, null, - null, null, null, null, null, null, null)); + null, null, null, null, null, null, null, null)); dispatchEvent(startEvent); AssistantMessageEvent msgEvent = createAssistantMessageEvent("Test content"); @@ -857,7 +857,7 @@ private SessionStartEvent createSessionStartEvent() { private SessionStartEvent createSessionStartEvent(String sessionId) { var event = new SessionStartEvent(); var data = new SessionStartEvent.SessionStartEventData(sessionId, null, null, null, null, null, null, null, - null, null, null, null, null); + null, null, null, null, null, null); event.setData(data); return event; } diff --git a/java/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java b/java/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java index b5e83f17dc..7df3562e32 100644 --- a/java/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java +++ b/java/src/test/java/com/github/copilot/generated/rpc/GeneratedRpcRecordsCoverageTest.java @@ -803,7 +803,7 @@ void modelsListResult_nested() { var limits = new ModelCapabilitiesLimits(100000L, 8192L, 128000L, null); var capabilities = new ModelCapabilities(supports, limits); var policy = new ModelPolicy(ModelPolicyState.ENABLED, null); - var billing = new ModelBilling(1.0, null); + var billing = new ModelBilling(1.0, null, null); var modelItem = new Model("gpt-5", "GPT-5", capabilities, policy, billing, null, null, null, null); var result = new ModelsListResult(List.of(modelItem)); diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 53686a6ca3..613985103f 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1326,7 +1326,8 @@ export class CopilotClient { sessionId, this.connection!, undefined, - this.onGetTraceContext + this.onGetTraceContext, + { mcpAuthHandler: config.onMcpAuthRequest } ); s.registerTools(config.tools); s.registerCanvases(config.canvases); @@ -1473,6 +1474,12 @@ export class CopilotClient { session = initializeSession(returnedSessionId); registeredId = returnedSessionId; } + if (config.onMcpAuthRequest) { + await this.connection!.sendRequest("session.eventLog.registerInterest", { + sessionId: returnedSessionId, + eventType: "mcp.oauth_required", + }); + } session["_workspacePath"] = workspacePath; session.setCapabilities(capabilities); @@ -1522,7 +1529,8 @@ export class CopilotClient { sessionId, this.connection!, undefined, - this.onGetTraceContext + this.onGetTraceContext, + { mcpAuthHandler: config.onMcpAuthRequest } ); session.registerTools(config.tools); session.registerCanvases(config.canvases); @@ -1567,6 +1575,12 @@ export class CopilotClient { } this.sessions.set(sessionId, session); this.setupSessionFs(session, config); + if (config.onMcpAuthRequest) { + await this.connection!.sendRequest("session.eventLog.registerInterest", { + sessionId, + eventType: "mcp.oauth_required", + }); + } const toolFilterOptions = this.resolveToolFilterOptions(config); diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 38f77412d9..b3339fffef 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -5,7 +5,7 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; -import type { AbortReason, Attachment, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval } from "./session-events.js"; +import type { AbortReason, Attachment, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, ResponseBudgetConfig, SessionEvent, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval } from "./session-events.js"; /** * Initial authentication info for the session. @@ -681,6 +681,26 @@ export type McpServerConfigHttpOauthGrantType = | "authorization_code" /** Headless client credentials flow using the configured OAuth client. */ | "client_credentials"; +/** + * Host response: supply dynamic headers or decline this refresh. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpHeadersHandlePendingHeadersRefreshRequest". + */ +/** @experimental */ +export type McpHeadersHandlePendingHeadersRefreshRequest = + | { + /** + * Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers. + */ + headers: { + [k: string]: string | undefined; + }; + kind: "headers"; + } + | { + kind: "none"; + }; /** * Host response to the pending OAuth request. * @@ -698,10 +718,6 @@ export type McpOauthPendingRequestResponse = * OAuth token type. Defaults to Bearer when omitted. */ tokenType?: string; - /** - * Refresh token supplied by the host, if available. - */ - refreshToken?: string; /** * Token lifetime in seconds, if known. */ @@ -1580,7 +1596,7 @@ export type SkillDiscoveryScope = /** A configured custom skill directory. */ | "custom"; /** - * Result of invoking the slash command (text output, prompt to send to the agent, or completion). + * Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). * * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema * via the `definition` "SlashCommandInvocationResult". @@ -1609,6 +1625,14 @@ export type SubagentSettings = { * Names of subagents the user has turned off; they cannot be dispatched */ disabledSubagents?: string[]; + /** + * Maximum number of subagents that can run concurrently; applies to usage-based billing users only + */ + maxConcurrency?: number; + /** + * Maximum subagent nesting depth; applies to usage-based billing users only + */ + maxDepth?: number; } | null; /** * Context tier override for matching subagents @@ -5539,6 +5563,33 @@ export interface McpFilteredServer { */ enterpriseName?: string; } +/** + * MCP headers refresh request id and the host response. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpHeadersHandlePendingHeadersRefreshRequestRequest". + */ +/** @experimental */ +export interface McpHeadersHandlePendingHeadersRefreshRequestRequest { + /** + * Headers refresh request identifier from mcp.headers_refresh_required + */ + requestId: string; + result: McpHeadersHandlePendingHeadersRefreshRequest; +} +/** + * Indicates whether the pending MCP headers refresh response was accepted. + * + * This interface was referenced by `_RpcSchemaRoot`'s JSON-Schema + * via the `definition` "McpHeadersHandlePendingHeadersRefreshRequestResult". + */ +/** @experimental */ +export interface McpHeadersHandlePendingHeadersRefreshRequestResult { + /** + * Whether the response was accepted. False if the request was unknown, timed out, or already resolved. + */ + success: boolean; +} /** * Host-level state, omitted when no MCP host is initialized. * @@ -6357,6 +6408,10 @@ export interface ModelBilling { */ multiplier?: number; tokenPrices?: ModelBillingTokenPrices; + /** + * Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. + */ + discountPercent?: number; } /** * Token-level pricing information for this model @@ -10617,6 +10672,10 @@ export interface SessionOpenOptions { */ logInteractiveShells?: boolean; envValueMode?: SessionOpenOptionsEnvValueMode; + /** + * Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. + */ + allowAllMcpServerInstructions?: boolean; /** * Additional directories to search for skills. */ @@ -10684,6 +10743,7 @@ export interface SessionOpenOptions { */ maxInlineBinaryBytes?: number; modelCapabilitiesOverrides?: ModelCapabilitiesOverride; + responseBudget?: ResponseBudgetConfig; /** * Runtime context discriminator for agent filtering. */ @@ -11580,6 +11640,10 @@ export interface SessionUpdateOptionsParams { */ logInteractiveShells?: boolean; envValueMode?: OptionsUpdateEnvValueMode; + /** + * Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. + */ + allowAllMcpServerInstructions?: boolean; /** * Additional directories to search for skills. */ @@ -11695,6 +11759,10 @@ export interface SessionUpdateOptionsParams { */ enableSkills?: boolean; contextTier?: OptionsUpdateContextTier; + /** + * Optional experimental response budget limits. Pass null to clear the response budget. + */ + responseBudget?: ResponseBudgetConfig | null; } /** * Indicates whether the session options patch was applied successfully. @@ -15018,6 +15086,18 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin connection.sendRequest("session.mcp.oauth.login", { sessionId, ...params }), }, /** @experimental */ + headers: { + /** + * Responds to a pending MCP dynamic headers refresh request. Hosts that subscribe to `mcp.headers_refresh_required` use this to provide short-lived per-server headers or to indicate that no dynamic headers are available for this refresh. + * + * @param params MCP headers refresh request id and the host response. + * + * @returns Indicates whether the pending MCP headers refresh response was accepted. + */ + handlePendingHeadersRefreshRequest: async (params: McpHeadersHandlePendingHeadersRefreshRequestRequest): Promise => + connection.sendRequest("session.mcp.headers.handlePendingHeadersRefreshRequest", { sessionId, ...params }), + }, + /** @experimental */ apps: { /** * Fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) from a connected server. Requires the `mcp-apps` session capability. @@ -15218,7 +15298,7 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin * * @param params Slash command name and optional raw input string to invoke. * - * @returns Result of invoking the slash command (text output, prompt to send to the agent, or completion). + * @returns Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). */ invoke: async (params: CommandsInvokeRequest): Promise => connection.sendRequest("session.commands.invoke", { sessionId, ...params }), diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 96a3bddac7..08c9fb3f84 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -45,6 +45,7 @@ export type SessionEvent = | AssistantMessageStartEvent | AssistantMessageDeltaEvent | AssistantTurnEndEvent + | AssistantIdleEvent | AssistantUsageEvent | ModelCallFailureEvent | AbortEvent @@ -75,6 +76,8 @@ export type SessionEvent = | SamplingCompletedEvent | McpOauthRequiredEvent | McpOauthCompletedEvent + | McpHeadersRefreshRequiredEvent + | McpHeadersRefreshCompletedEvent | CustomNotificationEvent | ExternalToolRequestedEvent | ExternalToolCompletedEvent @@ -234,6 +237,16 @@ export type AttachmentGitHubReferenceType = | "pr" /** GitHub discussion reference. */ | "discussion"; +/** + * How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn. + */ +export type UserMessageDelivery = + /** Delivered while the loop was idle; starts its own run immediately (a human's fresh turn, or a system notification waking an idle agent). */ + | "idle" + /** Injected into the current in-flight run while the agent was busy (immediate mode). */ + | "steering" + /** Enqueued while the agent was busy; processed as its own run afterward. */ + | "queued"; /** * The system that produced a citation. */ @@ -507,6 +520,18 @@ export type ElicitationCompletedAction = | "decline" /** The user dismissed the request. */ | "cancel"; +/** + * Reason the runtime is requesting host-provided MCP OAuth credentials + */ +export type McpOauthRequestReason = + /** Initial credentials are required before connecting to the MCP server. */ + | "initial" + /** The current host-provided credential was rejected and a replacement is requested. */ + | "refresh" + /** The server requires a new host authorization flow before continuing. */ + | "reauth" + /** The server requires a credential with additional scope or audience. */ + | "upscope"; /** * How the pending MCP OAuth request was completed */ @@ -515,6 +540,26 @@ export type McpOauthCompletionOutcome = | "token" /** The request completed without an OAuth provider. */ | "cancelled"; +/** + * Why dynamic headers are being requested. + */ +export type McpHeadersRefreshRequiredReason = + /** The transport is making its first dynamic header request for this server. */ + | "startup" + /** The previously cached dynamic headers expired. */ + | "ttl-expired" + /** The server returned 401 and stale dynamic headers were invalidated. */ + | "auth-failed"; +/** + * How the pending MCP headers refresh request resolved. + */ +export type McpHeadersRefreshCompletedOutcome = + /** The host supplied dynamic headers. */ + | "headers" + /** The host responded with no dynamic headers. */ + | "none" + /** No response arrived within the bounded window. */ + | "timeout"; /** * The user's auto-mode-switch choice */ @@ -684,6 +729,7 @@ export interface StartData { * Whether this session supports remote steering via GitHub */ remoteSteerable?: boolean; + responseBudget?: ResponseBudgetConfig; /** * Model selected at session creation time, if any */ @@ -735,6 +781,19 @@ export interface WorkingDirectoryContext { */ repositoryHost?: string; } +/** + * Optional response budget limits. + */ +export interface ResponseBudgetConfig { + /** + * Maximum AI Credits allowed while responding to one top-level user message. + */ + maxAiCredits?: number; + /** + * Maximum model-call iterations allowed while responding to one top-level user message. + */ + maxModelIterations?: number; +} /** * Session event "session.resume". Session resume metadata including current context and event count */ @@ -799,6 +858,10 @@ export interface ResumeData { * Whether this session supports remote steering via GitHub */ remoteSteerable?: boolean; + /** + * Response budget limits currently configured at resume time; null when no budget is active + */ + responseBudget?: ResponseBudgetConfig | null; /** * ISO 8601 timestamp when the session was resumed */ @@ -2322,6 +2385,7 @@ export interface UserMessageData { * The user's message text as displayed in the timeline */ content: string; + delivery?: UserMessageDelivery; /** * CAPI interaction ID for correlating this user message with its turn */ @@ -3219,6 +3283,45 @@ export interface AssistantTurnEndData { */ turnId: string; } +/** + * Session event "assistant.idle". Payload emitted whenever the main agent's processing loop goes idle, including while related background work (running agents or in-flight attached shell commands) is still pending and the session-level idle event is therefore deferred + */ +export interface AssistantIdleEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: AssistantIdleData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + 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 discriminator. Always "assistant.idle". + */ + type: "assistant.idle"; +} +/** + * Payload emitted whenever the main agent's processing loop goes idle, including while related background work (running agents or in-flight attached shell commands) is still pending and the session-level idle event is therefore deferred + */ +export interface AssistantIdleData { + /** + * True when the preceding agentic loop was cancelled via abort signal + */ + aborted?: boolean; +} /** * Session event "assistant.usage". LLM API call usage metrics including tokens, costs, quotas, and billing information */ @@ -3712,6 +3815,7 @@ export interface ToolExecutionStartData { * Tool call ID of the parent tool invocation when this event originates from a sub-agent */ parentToolCallId?: string; + shellToolInfo?: ToolExecutionStartShellToolInfo; /** * Unique identifier for this tool call */ @@ -3726,6 +3830,19 @@ export interface ToolExecutionStartData { */ turnId?: string; } +/** + * Shell-aware path hints for a shell tool's command, captured at start time so consumers can snapshot a file's pre-image before the tool runs. + */ +export interface ToolExecutionStartShellToolInfo { + /** + * Whether the command includes a file write redirection (e.g., > or >>). + */ + hasWriteFileRedirection: boolean; + /** + * File paths the command may read or write, derived from the command at start time. Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. + */ + possiblePaths: string[]; +} /** * Tool definition metadata, present for MCP tools with MCP Apps support */ @@ -6407,6 +6524,7 @@ export interface McpOauthRequiredEvent { * OAuth authentication request for an MCP server */ export interface McpOauthRequiredData { + reason: McpOauthRequestReason; /** * Unique identifier for this OAuth request; used to respond via session.mcp.oauth.handlePendingRequest */ @@ -6434,6 +6552,10 @@ export interface McpOauthRequiredStaticClientConfig { * OAuth client ID for the server */ clientId: string; + /** + * Optional OAuth client secret for confidential static clients, when the runtime can resolve one + */ + clientSecret?: string; /** * Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). */ @@ -6452,9 +6574,9 @@ export interface McpOauthWWWAuthenticateParams { */ error?: string; /** - * Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter + * Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter, if present */ - resourceMetadataUrl: string; + resourceMetadataUrl?: string; /** * Requested OAuth scopes from the WWW-Authenticate scope parameter, if present */ @@ -6500,6 +6622,94 @@ export interface McpOauthCompletedData { */ requestId: string; } +/** + * Session event "mcp.headers_refresh_required". Dynamic headers refresh request for a remote MCP server + */ +export interface McpHeadersRefreshRequiredEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: McpHeadersRefreshRequiredData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + 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 discriminator. Always "mcp.headers_refresh_required". + */ + type: "mcp.headers_refresh_required"; +} +/** + * Dynamic headers refresh request for a remote MCP server + */ +export interface McpHeadersRefreshRequiredData { + reason: McpHeadersRefreshRequiredReason; + /** + * Unique identifier for this headers refresh request; used to respond via session.mcp.headers.handlePendingHeadersRefreshRequest() + */ + requestId: string; + /** + * Display name of the remote MCP server requesting headers + */ + serverName: string; + /** + * URL of the remote MCP server requesting headers + */ + serverUrl: string; +} +/** + * Session event "mcp.headers_refresh_completed". MCP headers refresh request completion notification + */ +export interface McpHeadersRefreshCompletedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: McpHeadersRefreshCompletedData; + /** + * Always true for events that are transient and not persisted to the session event log on disk. + */ + 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 discriminator. Always "mcp.headers_refresh_completed". + */ + type: "mcp.headers_refresh_completed"; +} +/** + * MCP headers refresh request completion notification + */ +export interface McpHeadersRefreshCompletedData { + outcome: McpHeadersRefreshCompletedOutcome; + /** + * Request ID of the resolved headers refresh request + */ + requestId: string; +} /** * Session event "session.custom_notification". Opaque custom notification data. Consumers may branch on source and name, but payload semantics are source-defined. */ diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index 8bf9589c39..8d8fc6714f 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -10,7 +10,11 @@ import type { MessageConnection } from "vscode-jsonrpc/node.js"; import { ConnectionError, ErrorCodes, ResponseError } from "vscode-jsonrpc/node.js"; import { createSessionRpc } from "./generated/rpc.js"; -import type { ClientSessionApiHandlers, CanvasActionInvokeResult } from "./generated/rpc.js"; +import type { + ClientSessionApiHandlers, + CanvasActionInvokeResult, + McpOauthPendingRequestResponse, +} from "./generated/rpc.js"; import { type Canvas, CanvasError } from "./canvas.js"; import type { OpenCanvasInstance } from "./generated/rpc.js"; import { getTraceContext } from "./telemetry.js"; @@ -29,6 +33,8 @@ import type { BearerTokenProvider, UiInputOptions, MessageOptions, + McpAuthHandler, + McpAuthRequest, PermissionHandler, PermissionRequest, ContextTier, @@ -124,6 +130,7 @@ export class CopilotSession { private bearerTokenProviders: Map = new Map(); private commandHandlers: Map = new Map(); private permissionHandler?: PermissionHandler; + private mcpAuthHandler?: McpAuthHandler; private userInputHandler?: UserInputHandler; private elicitationHandler?: ElicitationHandler; private exitPlanModeHandler?: ExitPlanModeHandler; @@ -152,9 +159,11 @@ export class CopilotSession { public readonly sessionId: string, private connection: MessageConnection, private _workspacePath?: string, - traceContextProvider?: TraceContextProvider + traceContextProvider?: TraceContextProvider, + options?: { mcpAuthHandler?: McpAuthHandler } ) { this.traceContextProvider = traceContextProvider; + this.mcpAuthHandler = options?.mcpAuthHandler; } /** @@ -499,6 +508,19 @@ export class CopilotSession { if (this.permissionHandler) { void this._executePermissionAndRespond(requestId, permissionRequest); } + } else if (event.type === "mcp.oauth_required") { + const data = event.data as McpAuthRequest | undefined; + if (!data?.requestId) { + return; + } + if (!this.mcpAuthHandler) { + console.warn( + "Received MCP OAuth request without a registered MCP auth handler. " + + `SessionId=${this.sessionId}, RequestId=${data.requestId}` + ); + return; + } + void this._executeMcpAuthAndRespond(data); } else if (event.type === "command.execute") { const { requestId, commandName, command, args } = event.data as { requestId: string; @@ -661,6 +683,35 @@ export class CopilotSession { } } + /** + * Executes an MCP auth handler and sends the result back via RPC. + * @internal + */ + private async _executeMcpAuthAndRespond(request: McpAuthRequest): Promise { + try { + const result = await this.mcpAuthHandler!(request, { sessionId: this.sessionId }); + const response: McpOauthPendingRequestResponse = + result && "accessToken" in result + ? { kind: "token", ...result } + : { kind: "cancelled" }; + await this.rpc.mcp.oauth.handlePendingRequest({ + requestId: request.requestId, + result: response, + }); + } catch (_error) { + try { + await this.rpc.mcp.oauth.handlePendingRequest({ + requestId: request.requestId, + result: { kind: "cancelled" }, + }); + } catch (rpcError) { + if (!(rpcError instanceof ConnectionError || rpcError instanceof ResponseError)) { + throw rpcError; + } + } + } + } + /** * Executes a command handler and sends the result back via RPC. * @internal diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index e354bd8218..4adb35b25a 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -1615,6 +1615,76 @@ export type ReasoningEffort = "low" | "medium" | "high" | "xhigh"; */ export type ContextTier = "default" | "long_context"; +/** Parsed parameters from an MCP server's WWW-Authenticate response. */ +export interface McpAuthWwwAuthenticateParams { + /** Parsed resource_metadata URL used for protected-resource metadata discovery, if present. */ + resourceMetadataUrl?: string; + /** Parsed OAuth scope, if present. */ + scope?: string; + /** Parsed OAuth error, if present. */ + error?: string; +} + +/** Static OAuth client configuration supplied by the MCP server, if available. */ +export interface McpAuthStaticClientConfig { + /** OAuth client ID for the server. */ + clientId: string; + /** Optional OAuth client secret for confidential static clients. */ + clientSecret?: string; + /** Optional non-default OAuth grant type. */ + grantType?: "client_credentials"; + /** Whether this is a public OAuth client. */ + publicClient?: boolean; +} + +/** MCP OAuth request that the SDK host can satisfy with a host-acquired token. */ +export interface McpAuthRequest { + /** Unique request identifier used by the SDK when responding. */ + requestId: string; + /** Display name of the MCP server that requires OAuth. */ + serverName: string; + /** URL of the MCP server that requires OAuth. */ + serverUrl: string; + /** Why the runtime is requesting host-provided OAuth credentials. */ + reason: "initial" | "refresh" | "reauth" | "upscope"; + /** Parsed WWW-Authenticate parameters from the MCP server. */ + wwwAuthenticateParams?: McpAuthWwwAuthenticateParams; + /** Raw RFC 9728 protected-resource metadata JSON fetched by the runtime, if available. */ + resourceMetadata?: string; + /** Static OAuth client configuration, if the server specifies one. */ + staticClientConfig?: McpAuthStaticClientConfig; +} + +/** Host-provided OAuth token data for a pending MCP OAuth request. */ +export interface McpAuthToken { + /** Access token acquired by the SDK host. */ + accessToken: string; + /** OAuth token type. Defaults to Bearer when omitted. */ + tokenType?: string; + /** Token lifetime in seconds, if known. */ + expiresIn?: number; +} + +/** + * Result returned by an MCP auth request handler. + * + * Return `null`/`undefined` or `{ kind: "cancelled" }` to cancel the pending + * OAuth request. Return `{ kind: "token", ... }` to provide host-acquired + * OAuth token data. + */ +export type McpAuthResult = ({ kind: "token" } & McpAuthToken) | { kind: "cancelled" }; + +/** Callback invoked when an MCP server requires OAuth and the SDK host opted in. */ +export type McpAuthHandler = ( + request: McpAuthRequest, + context: { sessionId: string } +) => + | McpAuthResult + | McpAuthToken + | null + | undefined + | Promise; + /** * Stable extension identity for session participants that provide canvases. */ @@ -1898,6 +1968,13 @@ export interface SessionConfigBase { */ onPermissionRequest?: PermissionHandler; + /** + * Optional handler for MCP OAuth requests from MCP servers. + * When provided, the SDK can satisfy MCP server OAuth requests with + * host-provided token data or cancellation. + */ + onMcpAuthRequest?: McpAuthHandler; + /** * Handler for user input requests from the agent. * When provided, enables the ask_user tool allowing the agent to ask questions. diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 96d7da30cf..1ab3da604d 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -41,6 +41,228 @@ describe("CopilotClient", () => { expect(spy).not.toHaveBeenCalled(); }); + it("responds to MCP OAuth requests with host token data", async () => { + const sendRequest = vi.fn(async () => ({ success: true })); + let observedRequest: any; + const session = new CopilotSession( + "session-1", + { sendRequest } as any, + undefined, + undefined, + { + mcpAuthHandler: async (request) => { + observedRequest = request; + return { + accessToken: "host-token", + tokenType: "Bearer", + expiresIn: 3600, + }; + }, + } + ); + + await (session as any)._executeMcpAuthAndRespond({ + requestId: "oauth-request", + serverName: "oauth-server", + serverUrl: "https://example.com/mcp", + reason: "initial", + wwwAuthenticateParams: { + resourceMetadataUrl: "https://example.com/.well-known/oauth-protected-resource", + }, + resourceMetadata: '{"resource":"https://example.com/mcp"}', + staticClientConfig: { + clientId: "static-client", + clientSecret: "static-secret", + grantType: "client_credentials", + publicClient: false, + }, + }); + + expect(observedRequest.resourceMetadata).toBe('{"resource":"https://example.com/mcp"}'); + expect(observedRequest.staticClientConfig).toEqual({ + clientId: "static-client", + clientSecret: "static-secret", + grantType: "client_credentials", + publicClient: false, + }); + expect(sendRequest).toHaveBeenCalledWith("session.mcp.oauth.handlePendingRequest", { + sessionId: "session-1", + requestId: "oauth-request", + result: { + kind: "token", + accessToken: "host-token", + tokenType: "Bearer", + expiresIn: 3600, + }, + }); + }); + + it("passes MCP OAuth requests through when optional metadata is absent", async () => { + let observedRequest: any; + const session = new CopilotSession( + "session-1", + { sendRequest: vi.fn(async () => ({ success: true })) } as any, + undefined, + undefined, + { + mcpAuthHandler: async (request) => { + observedRequest = request; + return { kind: "cancelled" }; + }, + } + ); + + await (session as any)._executeMcpAuthAndRespond({ + requestId: "oauth-request", + serverName: "oauth-server", + serverUrl: "https://example.com/mcp", + reason: "initial", + }); + + expect(observedRequest.reason).toBe("initial"); + expect(observedRequest.resourceMetadata).toBeUndefined(); + expect(observedRequest.wwwAuthenticateParams).toBeUndefined(); + }); + + it("registers interest in MCP OAuth required events after create when an auth handler is configured", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.eventLog.registerInterest") { + return { id: "interest-1" }; + } + if (method === "session.create") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + await client.createSession({ + onPermissionRequest: approveAll, + onMcpAuthRequest: () => ({ kind: "cancelled" }), + }); + + expect(spy.mock.calls[0][0]).toBe("session.create"); + expect(spy.mock.calls[1]).toEqual([ + "session.eventLog.registerInterest", + expect.objectContaining({ eventType: "mcp.oauth_required" }), + ]); + expect(spy.mock.calls[1][1].sessionId).toBe(spy.mock.calls[0][1].sessionId); + }); + + it("does not register MCP OAuth interest without an auth handler", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + await client.createSession({ + onPermissionRequest: approveAll, + onEvent: () => {}, + }); + + expect(spy).not.toHaveBeenCalledWith( + "session.eventLog.registerInterest", + expect.objectContaining({ eventType: "mcp.oauth_required" }) + ); + expect(spy).toHaveBeenCalledWith( + "session.create", + expect.objectContaining({ requestPermission: true }) + ); + }); + + it("registers MCP OAuth interest after cloud create only when an auth handler is configured", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + let cloudCreateCount = 0; + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.eventLog.registerInterest") { + return { id: "interest-1" }; + } + if (method === "session.create") + return { sessionId: `server-assigned-session-${++cloudCreateCount}` }; + throw new Error(`Unexpected method: ${method}`); + }); + + await client.createSession({ + onPermissionRequest: approveAll, + cloud: { repository: { owner: "github", name: "copilot-sdk", branch: "main" } }, + }); + + expect(spy).not.toHaveBeenCalledWith( + "session.eventLog.registerInterest", + expect.objectContaining({ eventType: "mcp.oauth_required" }) + ); + + spy.mockClear(); + await client.createSession({ + onPermissionRequest: approveAll, + onMcpAuthRequest: () => ({ kind: "cancelled" }), + cloud: { repository: { owner: "github", name: "copilot-sdk", branch: "main" } }, + }); + + expect(spy.mock.calls[0][0]).toBe("session.create"); + expect(spy.mock.calls[1]).toEqual([ + "session.eventLog.registerInterest", + { sessionId: "server-assigned-session-2", eventType: "mcp.oauth_required" }, + ]); + }); + + it("registers MCP OAuth interest before resuming only when an auth handler is configured", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.eventLog.registerInterest") { + return { id: "interest-1" }; + } + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + await client.resumeSession("session-with-auth", { + onPermissionRequest: approveAll, + onMcpAuthRequest: () => ({ kind: "cancelled" }), + }); + + expect(spy.mock.calls[0]).toEqual([ + "session.eventLog.registerInterest", + { sessionId: "session-with-auth", eventType: "mcp.oauth_required" }, + ]); + expect(spy.mock.calls[1][0]).toBe("session.resume"); + expect(spy.mock.calls[1][1]).toEqual(expect.objectContaining({ requestPermission: true })); + + spy.mockClear(); + await client.resumeSession("session-without-auth", { + onPermissionRequest: approveAll, + onEvent: () => {}, + }); + + expect(spy).not.toHaveBeenCalledWith( + "session.eventLog.registerInterest", + expect.objectContaining({ eventType: "mcp.oauth_required" }) + ); + expect(spy).toHaveBeenCalledWith( + "session.resume", + expect.objectContaining({ sessionId: "session-without-auth", requestPermission: true }) + ); + }); + it("forwards canvas declarations and request flags in session.create", async () => { const client = new CopilotClient(); await client.start(); diff --git a/nodejs/test/e2e/harness/sdkTestContext.ts b/nodejs/test/e2e/harness/sdkTestContext.ts index cd6494cad3..373b25352e 100644 --- a/nodejs/test/e2e/harness/sdkTestContext.ts +++ b/nodejs/test/e2e/harness/sdkTestContext.ts @@ -19,6 +19,18 @@ export const DEFAULT_GITHUB_TOKEN = "fake-token-for-e2e-tests"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const SNAPSHOTS_DIR = resolve(__dirname, "../../../../test/snapshots"); +const LOCAL_RUNTIME_CLI_PATH = + "/Users/roji/.copilot/repos/copilot-worktrees/copilot-agent-runtime/roji-symmetrical-dollop/dist-cli/index.js"; + +function getCliPathForTests(): string | undefined { + if (process.env.COPILOT_CLI_PATH) { + return process.env.COPILOT_CLI_PATH; + } + if (fs.existsSync(LOCAL_RUNTIME_CLI_PATH)) { + return LOCAL_RUNTIME_CLI_PATH; + } + return undefined; +} export async function createSdkTestContext({ logLevel, @@ -39,6 +51,7 @@ export async function createSdkTestContext({ await openAiEndpoint.setCopilotUserByToken(DEFAULT_GITHUB_TOKEN, { login: "e2e-test-user", copilot_plan: "individual_pro", + is_mcp_enabled: true, endpoints: { api: proxyUrl, telemetry: "https://localhost:1/telemetry", @@ -72,6 +85,7 @@ export async function createSdkTestContext({ }; const userConn = copilotClientOptions?.connection; + const cliPath = getCliPathForTests(); let connection: RuntimeConnection; if (userConn) { // Caller supplied a RuntimeConnection — merge in the harness-managed @@ -82,13 +96,13 @@ export async function createSdkTestContext({ const { kind: _k, ...tcp } = userConn; connection = RuntimeConnection.forTcp({ ...tcp, - path: tcp.path ?? process.env.COPILOT_CLI_PATH, + path: tcp.path ?? cliPath, }); } else if (userConn.kind === "stdio") { const { kind: _k, ...stdio } = userConn; connection = RuntimeConnection.forStdio({ ...stdio, - path: stdio.path ?? process.env.COPILOT_CLI_PATH, + path: stdio.path ?? cliPath, }); } else { connection = userConn; @@ -96,15 +110,18 @@ export async function createSdkTestContext({ } else { connection = useStdio === false - ? RuntimeConnection.forTcp({ path: process.env.COPILOT_CLI_PATH }) - : RuntimeConnection.forStdio({ path: process.env.COPILOT_CLI_PATH }); + ? RuntimeConnection.forTcp({ path: cliPath }) + : RuntimeConnection.forStdio({ path: cliPath }); } - const { connection: _ignoredConnection, ...remainingClientOptions } = - copilotClientOptions ?? {}; + const { + connection: _ignoredConnection, + env: userEnv, + ...remainingClientOptions + } = copilotClientOptions ?? {}; const copilotClient = new CopilotClient({ workingDirectory: workDir, - env, + env: { ...env, ...userEnv }, logLevel: logLevel || "error", connection, gitHubToken: authTokenToUse, diff --git a/nodejs/test/e2e/mcp_oauth.e2e.test.ts b/nodejs/test/e2e/mcp_oauth.e2e.test.ts new file mode 100644 index 0000000000..29ed089edb --- /dev/null +++ b/nodejs/test/e2e/mcp_oauth.e2e.test.ts @@ -0,0 +1,311 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { dirname, resolve } from "node:path"; +import { createInterface } from "node:readline"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it, onTestFinished } from "vitest"; +import type { CopilotSession, MCPServerConfig, McpAuthRequest } from "../../src/index.js"; +import { approveAll } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; +import { waitForCondition } from "./harness/sdkTestHelper.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const TEST_MCP_OAUTH_SERVER = resolve(__dirname, "../../../test/harness/test-mcp-oauth-server.mjs"); +const EXPECTED_TOKEN = "sdk-host-token"; +const REFRESH_TOKEN = `${EXPECTED_TOKEN}-refresh`; +const UPSCOPE_TOKEN = `${EXPECTED_TOKEN}-upscope`; +const REAUTH_TOKEN = `${EXPECTED_TOKEN}-reauth`; + +describe("MCP OAuth host auth", async () => { + const { copilotClient: client } = await createSdkTestContext({ + copilotClientOptions: { + env: { + COPILOT_MCP_APPS: "true", + MCP_APPS: "true", + }, + }, + }); + + it("should satisfy MCP OAuth using host-provided token", { timeout: 120_000 }, async () => { + const oauthServer = await startOAuthMcpServer(); + const serverName = "oauth-protected-mcp"; + let authRequest: McpAuthRequest | undefined; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + enableMcpApps: true, + onMcpAuthRequest: async (request) => { + authRequest = request; + return { + kind: "token", + accessToken: EXPECTED_TOKEN, + tokenType: "Bearer", + expiresIn: 3600, + }; + }, + mcpServers: { + [serverName]: { + type: "http", + url: `${oauthServer.url}/mcp`, + tools: ["*"], + oauthClientId: "sdk-e2e-client", + oauthPublicClient: true, + } as unknown as MCPServerConfig, + }, + }); + onTestFinished(() => disconnectSession(session)); + + await waitForMcpServerStatus(session, serverName); + + const tools = await session.rpc.mcp.listTools({ serverName }); + expect(tools.tools.map((tool) => tool.name)).toContain("whoami"); + + expect(authRequest).toMatchObject({ + requestId: expect.any(String), + serverName, + serverUrl: `${oauthServer.url}/mcp`, + reason: "initial", + wwwAuthenticateParams: { + resourceMetadataUrl: `${oauthServer.url}/.well-known/oauth-protected-resource`, + scope: "mcp.read", + error: "invalid_token", + }, + resourceMetadata: JSON.stringify({ + resource: `${oauthServer.url}/mcp`, + authorization_servers: [oauthServer.url], + scopes_supported: ["mcp.read"], + bearer_methods_supported: ["header"], + }), + }); + + const requests = await oauthServer.requests(); + expect(requests.some((request) => request.authorization === null)).toBe(true); + expect( + requests.some((request) => request.authorization === `Bearer ${EXPECTED_TOKEN}`) + ).toBe(true); + }); + + it( + "should request host-owned replacement tokens across the MCP OAuth lifecycle", + { timeout: 120_000 }, + async () => { + const oauthServer = await startOAuthMcpServer(); + const serverName = "oauth-lifecycle-mcp"; + const authRequests: McpAuthRequest[] = []; + let refreshCount = 0; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + enableMcpApps: true, + onMcpAuthRequest: async (request) => { + authRequests.push(request); + switch (request.reason) { + case "initial": + return { kind: "token", accessToken: EXPECTED_TOKEN }; + case "refresh": + refreshCount++; + if (refreshCount === 1) { + return { kind: "token", accessToken: REFRESH_TOKEN }; + } + return { kind: "cancelled" }; + case "upscope": + return { kind: "token", accessToken: UPSCOPE_TOKEN }; + case "reauth": + return { kind: "token", accessToken: REAUTH_TOKEN }; + } + }, + mcpServers: { + [serverName]: { + type: "http", + url: `${oauthServer.url}/mcp`, + tools: ["*"], + oauthClientId: "sdk-e2e-client", + oauthPublicClient: true, + } as unknown as MCPServerConfig, + }, + }); + onTestFinished(() => disconnectSession(session)); + + await waitForMcpServerStatus(session, serverName); + await callWhoami(session, serverName, "refresh"); + await callWhoami(session, serverName, "upscope"); + await callWhoami(session, serverName, "reauth"); + + expect(authRequests.map((request) => request.reason)).toEqual([ + "initial", + "refresh", + "upscope", + "refresh", + "reauth", + ]); + + const upscopeRequest = authRequests.find((request) => request.reason === "upscope"); + expect(upscopeRequest?.wwwAuthenticateParams).toEqual({ + resourceMetadataUrl: `${oauthServer.url}/.well-known/oauth-protected-resource`, + scope: "mcp.write", + error: "insufficient_scope", + }); + expect(upscopeRequest?.resourceMetadata).toBe( + JSON.stringify({ + resource: `${oauthServer.url}/mcp`, + authorization_servers: [oauthServer.url], + scopes_supported: ["mcp.read"], + bearer_methods_supported: ["header"], + }) + ); + + const requests = await oauthServer.requests(); + for (const token of [EXPECTED_TOKEN, REFRESH_TOKEN, UPSCOPE_TOKEN, REAUTH_TOKEN]) { + expect( + requests.some((request) => request.authorization === `Bearer ${token}`) + ).toBe(true); + } + } + ); + + it( + "should cancel pending MCP OAuth requests when the host declines", + { timeout: 120_000 }, + async () => { + const oauthServer = await startOAuthMcpServer(); + const serverName = "oauth-cancelled-mcp"; + let authRequest: McpAuthRequest | undefined; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + onMcpAuthRequest: async (request) => { + authRequest = request; + return { kind: "cancelled" }; + }, + mcpServers: { + [serverName]: { + type: "http", + url: `${oauthServer.url}/mcp`, + tools: ["*"], + oauthClientId: "sdk-e2e-client", + oauthPublicClient: true, + } as unknown as MCPServerConfig, + }, + }); + onTestFinished(() => disconnectSession(session)); + + await waitForMcpServerStatus(session, serverName, "failed"); + + expect(authRequest).toMatchObject({ + serverName, + reason: "initial", + }); + } + ); +}); + +async function waitForMcpServerStatus( + session: CopilotSession, + serverName: string, + expectedStatus = "connected" +): Promise { + let lastStatus = ""; + await waitForCondition( + async () => { + const result = await session.rpc.mcp.list(); + const server = result.servers.find((entry) => entry.name === serverName); + lastStatus = server?.status ?? ""; + return server?.status === expectedStatus; + }, + { + timeoutMs: 60_000, + intervalMs: 200, + timeoutMessage: `${serverName} did not reach ${expectedStatus}; last status was ${lastStatus}`, + } + ); +} + +async function callWhoami( + session: CopilotSession, + serverName: string, + scenario: "refresh" | "upscope" | "reauth" +): Promise { + const result = await session.rpc.mcp.apps.callTool({ + serverName, + originServerName: serverName, + toolName: "whoami", + arguments: { scenario }, + }); + expect(result.content).toEqual([{ type: "text", text: "oauth-test-user" }]); +} + +async function startOAuthMcpServer(): Promise<{ + url: string; + requests: () => Promise>; +}> { + const child = spawn(process.execPath, [TEST_MCP_OAUTH_SERVER], { + env: { ...process.env, EXPECTED_TOKEN }, + stdio: ["ignore", "pipe", "pipe"], + }); + onTestFinished(() => stopChild(child)); + + const stderr: string[] = []; + child.stderr.on("data", (chunk) => stderr.push(String(chunk))); + + const url = await new Promise((resolvePromise, reject) => { + const rl = createInterface({ input: child.stdout }); + const timeout = setTimeout(() => { + rl.close(); + reject(new Error(`Timed out waiting for OAuth MCP server. ${stderr.join("")}`)); + }, 10_000); + + child.once("exit", (code, signal) => { + clearTimeout(timeout); + rl.close(); + reject( + new Error( + `OAuth MCP server exited before listening. code=${code} signal=${signal} ${stderr.join("")}` + ) + ); + }); + + rl.on("line", (line) => { + const match = /^Listening: (.+)$/.exec(line); + if (!match) { + return; + } + clearTimeout(timeout); + rl.close(); + resolvePromise(match[1]); + }); + }); + + return { + url, + requests: async () => { + const response = await fetch(`${url}/__requests`); + if (!response.ok) { + throw new Error(`Failed to fetch OAuth MCP requests: ${response.status}`); + } + return response.json(); + }, + }; +} + +async function disconnectSession(session: CopilotSession): Promise { + try { + await session.disconnect(); + } catch { + // Best-effort cleanup. + } +} + +function stopChild(child: ChildProcessWithoutNullStreams): Promise { + if (child.exitCode !== null || child.killed) { + return Promise.resolve(); + } + const exitPromise = new Promise((resolvePromise) => { + child.once("exit", () => resolvePromise()); + }); + child.kill("SIGTERM"); + return exitPromise; +} diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index ff13d47de3..3ebc1786fa 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -107,6 +107,12 @@ MCPHTTPServerConfig, MCPServerConfig, MCPStdioServerConfig, + McpAuthHandler, + McpAuthRequest, + McpAuthResult, + McpAuthStaticClientConfig, + McpAuthToken, + McpAuthWwwAuthenticateParams, ModelCapabilitiesOverride, ModelLimitsOverride, ModelSupportsOverride, @@ -226,6 +232,12 @@ "MCPHTTPServerConfig", "MCPServerConfig", "MCPStdioServerConfig", + "McpAuthHandler", + "McpAuthRequest", + "McpAuthResult", + "McpAuthStaticClientConfig", + "McpAuthToken", + "McpAuthWwwAuthenticateParams", "ModelBilling", "ModelBillingTokenPrices", "ModelBillingTokenPricesLongContext", diff --git a/python/copilot/client.py b/python/copilot/client.py index c7d11d12b1..72c74a7c0a 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -94,6 +94,7 @@ LargeToolOutputConfig, MCPServerConfig, MemoryConfiguration, + McpAuthHandler, ModelCapabilitiesOverride, NamedProviderConfig, ProviderConfig, @@ -1697,6 +1698,7 @@ async def create_session( on_event: Callable[[SessionEvent], None] | None = None, commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, + on_mcp_auth_request: McpAuthHandler | None = None, enable_mcp_apps: bool = False, on_exit_plan_mode_request: ExitPlanModeHandler | None = None, on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, @@ -2149,6 +2151,7 @@ def _initialize_session(sid: str) -> CopilotSession: s._register_tools(tools) s._register_commands(commands) s._register_permission_handler(on_permission_request) + s._register_mcp_auth_handler(on_mcp_auth_request) if on_user_input_request: s._register_user_input_handler(on_user_input_request) if on_elicitation_request: @@ -2229,6 +2232,11 @@ def _register_inline(raw_response: Any) -> None: f"session.create returned sessionId {response.get('sessionId')} " f"but the caller requested {local_session_id}" ) + if on_mcp_auth_request is not None: + await self._client.request( + "session.eventLog.registerInterest", + {"sessionId": session.session_id, "eventType": "mcp.oauth_required"}, + ) session._workspace_path = response.get("workspacePath") capabilities = response.get("capabilities") session._set_capabilities(capabilities) @@ -2319,6 +2327,7 @@ async def resume_session( on_event: Callable[[SessionEvent], None] | None = None, commands: list[CommandDefinition] | None = None, on_elicitation_request: ElicitationHandler | None = None, + on_mcp_auth_request: McpAuthHandler | None = None, enable_mcp_apps: bool = False, on_exit_plan_mode_request: ExitPlanModeHandler | None = None, on_auto_mode_switch_request: AutoModeSwitchHandler | None = None, @@ -2723,6 +2732,7 @@ async def resume_session( session._register_tools(tools) session._register_commands(commands) session._register_permission_handler(on_permission_request) + session._register_mcp_auth_handler(on_mcp_auth_request) if on_user_input_request: session._register_user_input_handler(on_user_input_request) if on_elicitation_request: @@ -2744,6 +2754,11 @@ async def resume_session( session.on(on_event) with self._sessions_lock: self._sessions[session_id] = session + if on_mcp_auth_request is not None: + await self._client.request( + "session.eventLog.registerInterest", + {"sessionId": session_id, "eventType": "mcp.oauth_required"}, + ) log_timing( logger, logging.DEBUG, diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index b38c12ff39..23db13b4d7 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -6,7 +6,7 @@ from typing import ClassVar, TYPE_CHECKING -from .session_events import AbortReason, Attachment, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, SessionEvent, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval +from .session_events import AbortReason, Attachment, ContextTier, EmbeddedBlobResourceContents, EmbeddedTextResourceContents, McpServerSource, McpServerStatus, PermissionPromptRequest, PermissionRule, ReasoningSummary, ResponseBudgetConfig, SessionEvent, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval if TYPE_CHECKING: from .._jsonrpc import JsonRpcClient @@ -2982,6 +2982,31 @@ def to_dict(self) -> dict: result["redactedReason"] = from_union([from_str, from_none], self.redacted_reason) return result +class MCPHeadersHandlePendingHeadersRefreshRequestKind(Enum): + HEADERS = "headers" + NONE = "none" + +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPHeadersHandlePendingHeadersRefreshRequestResult: + """Indicates whether the pending MCP headers refresh response was accepted.""" + + success: bool + """Whether the response was accepted. False if the request was unknown, timed out, or + already resolved. + """ + + @staticmethod + def from_dict(obj: Any) -> 'MCPHeadersHandlePendingHeadersRefreshRequestResult': + assert isinstance(obj, dict) + success = from_bool(obj.get("success")) + return MCPHeadersHandlePendingHeadersRefreshRequestResult(success) + + def to_dict(self) -> dict: + result: dict = {} + result["success"] = from_bool(self.success) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPServerFailureInfo: @@ -11402,6 +11427,31 @@ def to_dict(self) -> dict: result["allowedServers"] = from_union([lambda x: from_list(lambda x: to_class(MCPAllowedServer, x), x), from_none], self.allowed_servers) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPHeadersHandlePendingHeadersRefreshRequest: + """Host response: supply dynamic headers or decline this refresh.""" + + kind: MCPHeadersHandlePendingHeadersRefreshRequestKind + headers: dict[str, str] | None = None + """Headers to overlay onto the MCP request. Dynamic headers override static config headers + but do not replace SDK-managed request headers. + """ + + @staticmethod + def from_dict(obj: Any) -> 'MCPHeadersHandlePendingHeadersRefreshRequest': + assert isinstance(obj, dict) + kind = MCPHeadersHandlePendingHeadersRefreshRequestKind(obj.get("kind")) + headers = from_union([lambda x: from_dict(from_str, x), from_none], obj.get("headers")) + return MCPHeadersHandlePendingHeadersRefreshRequest(kind, headers) + + def to_dict(self) -> dict: + result: dict = {} + result["kind"] = to_enum(MCPHeadersHandlePendingHeadersRefreshRequestKind, self.kind) + if self.headers is not None: + result["headers"] = from_union([lambda x: from_dict(from_str, x), from_none], self.headers) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPHostState: @@ -11463,9 +11513,6 @@ class MCPOauthPendingRequestResponse: expires_in: int | None = None """Token lifetime in seconds, if known.""" - refresh_token: str | None = None - """Refresh token supplied by the host, if available.""" - token_type: str | None = None """OAuth token type. Defaults to Bearer when omitted.""" @@ -11475,9 +11522,8 @@ def from_dict(obj: Any) -> 'MCPOauthPendingRequestResponse': kind = MCPOauthPendingRequestResponseKind(obj.get("kind")) access_token = from_union([from_str, from_none], obj.get("accessToken")) expires_in = from_union([from_int, from_none], obj.get("expiresIn")) - refresh_token = from_union([from_str, from_none], obj.get("refreshToken")) token_type = from_union([from_str, from_none], obj.get("tokenType")) - return MCPOauthPendingRequestResponse(kind, access_token, expires_in, refresh_token, token_type) + return MCPOauthPendingRequestResponse(kind, access_token, expires_in, token_type) def to_dict(self) -> dict: result: dict = {} @@ -11486,8 +11532,6 @@ def to_dict(self) -> dict: result["accessToken"] = from_union([from_str, from_none], self.access_token) if self.expires_in is not None: result["expiresIn"] = from_union([from_int, from_none], self.expires_in) - if self.refresh_token is not None: - result["refreshToken"] = from_union([from_str, from_none], self.refresh_token) if self.token_type is not None: result["tokenType"] = from_union([from_str, from_none], self.token_type) return result @@ -16791,6 +16835,30 @@ def to_dict(self) -> dict: result["serverName"] = from_str(self.server_name) return result +# Experimental: this type is part of an experimental API and may change or be removed. +@dataclass +class MCPHeadersHandlePendingHeadersRefreshRequestRequest: + """MCP headers refresh request id and the host response.""" + + request_id: str + """Headers refresh request identifier from mcp.headers_refresh_required""" + + result: MCPHeadersHandlePendingHeadersRefreshRequest + """Host response: supply dynamic headers or decline this refresh.""" + + @staticmethod + def from_dict(obj: Any) -> 'MCPHeadersHandlePendingHeadersRefreshRequestRequest': + assert isinstance(obj, dict) + request_id = from_str(obj.get("requestId")) + result = MCPHeadersHandlePendingHeadersRefreshRequest.from_dict(obj.get("result")) + return MCPHeadersHandlePendingHeadersRefreshRequestRequest(request_id, result) + + def to_dict(self) -> dict: + result: dict = {} + result["requestId"] = from_str(self.request_id) + result["result"] = to_class(MCPHeadersHandlePendingHeadersRefreshRequest, self.result) + return result + # Experimental: this type is part of an experimental API and may change or be removed. @dataclass class MCPOauthHandlePendingRequest: @@ -16819,6 +16887,11 @@ def to_dict(self) -> dict: class ModelBilling: """Billing information""" + discount_percent: int | None = None + """Whole-number percentage discount (0-100) applied to usage billed through this model. + Populated for the synthetic `auto` model, where requests routed by auto-mode are billed + at a reduced rate; absent for concrete models. + """ multiplier: float | None = None """Billing cost multiplier relative to the base rate""" @@ -16828,12 +16901,15 @@ class ModelBilling: @staticmethod def from_dict(obj: Any) -> 'ModelBilling': assert isinstance(obj, dict) + discount_percent = from_union([from_int, from_none], obj.get("discountPercent")) multiplier = from_union([from_float, from_none], obj.get("multiplier")) token_prices = from_union([ModelBillingTokenPrices.from_dict, from_none], obj.get("tokenPrices")) - return ModelBilling(multiplier, token_prices) + return ModelBilling(discount_percent, multiplier, token_prices) def to_dict(self) -> dict: result: dict = {} + if self.discount_percent is not None: + result["discountPercent"] = from_union([from_int, from_none], self.discount_percent) if self.multiplier is not None: result["multiplier"] = from_union([to_float, from_none], self.multiplier) if self.token_prices is not None: @@ -19165,6 +19241,10 @@ class SessionOpenOptions: agent_context: str | None = None """Runtime context discriminator for agent filtering.""" + allow_all_mcp_server_instructions: bool | None = None + """Whether to include instructions from every MCP server in the system prompt instead of + only allowlisted servers. + """ ask_user_disabled: bool | None = None """Whether ask_user is explicitly disabled.""" @@ -19300,6 +19380,9 @@ class SessionOpenOptions: remote_steerable: bool | None = None """Whether this session supports remote steering.""" + response_budget: ResponseBudgetConfig | None = None + """Initial experimental response budget limits for the session.""" + running_in_interactive_mode: bool | None = None """Whether the host is an interactive UI.""" @@ -19338,6 +19421,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': assert isinstance(obj, dict) additional_content_exclusion_policies = from_union([lambda x: from_list(SessionOpenOptionsAdditionalContentExclusionPolicy.from_dict, x), from_none], obj.get("additionalContentExclusionPolicies")) agent_context = from_union([from_str, from_none], obj.get("agentContext")) + allow_all_mcp_server_instructions = from_union([from_bool, from_none], obj.get("allowAllMcpServerInstructions")) ask_user_disabled = from_union([from_bool, from_none], obj.get("askUserDisabled")) auth_info = from_union([_load_AuthInfo, from_none], obj.get("authInfo")) available_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("availableTools")) @@ -19380,6 +19464,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': remote_defaulted_on = from_union([from_bool, from_none], obj.get("remoteDefaultedOn")) remote_exporting = from_union([from_bool, from_none], obj.get("remoteExporting")) remote_steerable = from_union([from_bool, from_none], obj.get("remoteSteerable")) + response_budget = from_union([ResponseBudgetConfig.from_dict, from_none], obj.get("responseBudget")) running_in_interactive_mode = from_union([from_bool, from_none], obj.get("runningInInteractiveMode")) sandbox_config = from_union([SandboxConfig.from_dict, from_none], obj.get("sandboxConfig")) session_capabilities = from_union([lambda x: from_list(SessionCapability, x), from_none], obj.get("sessionCapabilities")) @@ -19391,7 +19476,7 @@ def from_dict(obj: Any) -> 'SessionOpenOptions': trajectory_file = from_union([from_str, from_none], obj.get("trajectoryFile")) working_directory = from_union([from_str, from_none], obj.get("workingDirectory")) working_directory_context = from_union([SessionContext.from_dict, from_none], obj.get("workingDirectoryContext")) - return SessionOpenOptions(additional_content_exclusion_policies, agent_context, ask_user_disabled, auth_info, available_tools, capi, client_kind, client_name, coauthor_enabled, config_dir, continue_on_auto_mode, copilot_url, custom_agents_local_only, detached_from_spawning_parent_engagement_id, detached_from_spawning_parent_session_id, disabled_instruction_sources, disabled_skills, enable_citations, enable_on_demand_instruction_discovery, enable_script_safety, enable_streaming, env_value_mode, events_log_directory, excluded_tools, exp_assignments, feature_flags, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, max_inline_binary_bytes, memory, model, model_capabilities_overrides, models, name, provider, providers, reasoning_effort, reasoning_summary, remote_defaulted_on, remote_exporting, remote_steerable, running_in_interactive_mode, sandbox_config, session_capabilities, session_id, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, working_directory, working_directory_context) + return SessionOpenOptions(additional_content_exclusion_policies, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, auth_info, available_tools, capi, client_kind, client_name, coauthor_enabled, config_dir, continue_on_auto_mode, copilot_url, custom_agents_local_only, detached_from_spawning_parent_engagement_id, detached_from_spawning_parent_session_id, disabled_instruction_sources, disabled_skills, enable_citations, enable_on_demand_instruction_discovery, enable_script_safety, enable_streaming, env_value_mode, events_log_directory, excluded_tools, exp_assignments, feature_flags, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, max_inline_binary_bytes, memory, model, model_capabilities_overrides, models, name, provider, providers, reasoning_effort, reasoning_summary, remote_defaulted_on, remote_exporting, remote_steerable, response_budget, running_in_interactive_mode, sandbox_config, session_capabilities, session_id, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, trajectory_file, working_directory, working_directory_context) def to_dict(self) -> dict: result: dict = {} @@ -19399,6 +19484,8 @@ def to_dict(self) -> dict: result["additionalContentExclusionPolicies"] = from_union([lambda x: from_list(lambda x: to_class(SessionOpenOptionsAdditionalContentExclusionPolicy, x), x), from_none], self.additional_content_exclusion_policies) if self.agent_context is not None: result["agentContext"] = from_union([from_str, from_none], self.agent_context) + if self.allow_all_mcp_server_instructions is not None: + result["allowAllMcpServerInstructions"] = from_union([from_bool, from_none], self.allow_all_mcp_server_instructions) if self.ask_user_disabled is not None: result["askUserDisabled"] = from_union([from_bool, from_none], self.ask_user_disabled) if self.auth_info is not None: @@ -19483,6 +19570,8 @@ def to_dict(self) -> dict: result["remoteExporting"] = from_union([from_bool, from_none], self.remote_exporting) if self.remote_steerable is not None: result["remoteSteerable"] = from_union([from_bool, from_none], self.remote_steerable) + if self.response_budget is not None: + result["responseBudget"] = from_union([lambda x: to_class(ResponseBudgetConfig, x), from_none], self.response_budget) if self.running_in_interactive_mode is not None: result["runningInInteractiveMode"] = from_union([from_bool, from_none], self.running_in_interactive_mode) if self.sandbox_config is not None: @@ -19518,6 +19607,10 @@ class SessionUpdateOptionsParams: agent_context: str | None = None """Runtime context discriminator (e.g., `cli`, `actions`).""" + allow_all_mcp_server_instructions: bool | None = None + """Whether to include instructions from every MCP server in the system prompt instead of + only allowlisted servers. + """ ask_user_disabled: bool | None = None """Whether to disable the `ask_user` tool (encourages autonomous behavior).""" @@ -19641,6 +19734,9 @@ class SessionUpdateOptionsParams: reasoning_summary: ReasoningSummary | None = None """Reasoning summary mode for supported model clients.""" + response_budget: ResponseBudgetConfig | None = None + """Optional experimental response budget limits. Pass null to clear the response budget.""" + running_in_interactive_mode: bool | None = None """Whether the session is running in an interactive UI.""" @@ -19687,6 +19783,7 @@ def from_dict(obj: Any) -> 'SessionUpdateOptionsParams': assert isinstance(obj, dict) additional_content_exclusion_policies = from_union([lambda x: from_list(OptionsUpdateAdditionalContentExclusionPolicy.from_dict, x), from_none], obj.get("additionalContentExclusionPolicies")) agent_context = from_union([from_str, from_none], obj.get("agentContext")) + allow_all_mcp_server_instructions = from_union([from_bool, from_none], obj.get("allowAllMcpServerInstructions")) ask_user_disabled = from_union([from_bool, from_none], obj.get("askUserDisabled")) available_tools = from_union([lambda x: from_list(from_str, x), from_none], obj.get("availableTools")) capi = from_union([CapiSessionOptions.from_dict, from_none], obj.get("capi")) @@ -19723,6 +19820,7 @@ def from_dict(obj: Any) -> 'SessionUpdateOptionsParams': provider = from_union([ProviderConfig.from_dict, from_none], obj.get("provider")) reasoning_effort = from_union([from_str, from_none], obj.get("reasoningEffort")) reasoning_summary = from_union([ReasoningSummary, from_none], obj.get("reasoningSummary")) + response_budget = from_union([ResponseBudgetConfig.from_dict, from_none], obj.get("responseBudget")) running_in_interactive_mode = from_union([from_bool, from_none], obj.get("runningInInteractiveMode")) sandbox_config = from_union([SandboxConfig.from_dict, from_none], obj.get("sandboxConfig")) session_capabilities = from_union([lambda x: from_list(SessionCapability, x), from_none], obj.get("sessionCapabilities")) @@ -19735,7 +19833,7 @@ def from_dict(obj: Any) -> 'SessionUpdateOptionsParams': tool_filter_precedence = from_union([OptionsUpdateToolFilterPrecedence, from_none], obj.get("toolFilterPrecedence")) trajectory_file = from_union([from_str, from_none], obj.get("trajectoryFile")) working_directory = from_union([from_str, from_none], obj.get("workingDirectory")) - return SessionUpdateOptionsParams(additional_content_exclusion_policies, agent_context, ask_user_disabled, available_tools, capi, client_name, coauthor_enabled, context_tier, continue_on_auto_mode, copilot_url, custom_agents_local_only, disabled_instruction_sources, disabled_skills, enable_file_hooks, enable_host_git_operations, enable_on_demand_instruction_discovery, enable_reasoning_summaries, enable_script_safety, enable_session_store, enable_skills, enable_streaming, env_value_mode, events_log_directory, excluded_tools, feature_flags, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, manage_schedule_enabled, max_inline_binary_bytes, model, model_capabilities_overrides, organization_custom_instructions, provider, reasoning_effort, reasoning_summary, running_in_interactive_mode, sandbox_config, session_capabilities, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, skip_embedding_retrieval, suppress_custom_agent_prompt, tool_filter_precedence, trajectory_file, working_directory) + return SessionUpdateOptionsParams(additional_content_exclusion_policies, agent_context, allow_all_mcp_server_instructions, ask_user_disabled, available_tools, capi, client_name, coauthor_enabled, context_tier, continue_on_auto_mode, copilot_url, custom_agents_local_only, disabled_instruction_sources, disabled_skills, enable_file_hooks, enable_host_git_operations, enable_on_demand_instruction_discovery, enable_reasoning_summaries, enable_script_safety, enable_session_store, enable_skills, enable_streaming, env_value_mode, events_log_directory, excluded_tools, feature_flags, installed_plugins, integration_id, is_experimental_mode, log_interactive_shells, lsp_client_name, manage_schedule_enabled, max_inline_binary_bytes, model, model_capabilities_overrides, organization_custom_instructions, provider, reasoning_effort, reasoning_summary, response_budget, running_in_interactive_mode, sandbox_config, session_capabilities, shell_init_profile, shell_process_flags, skill_directories, skip_custom_instructions, skip_embedding_retrieval, suppress_custom_agent_prompt, tool_filter_precedence, trajectory_file, working_directory) def to_dict(self) -> dict: result: dict = {} @@ -19743,6 +19841,8 @@ def to_dict(self) -> dict: result["additionalContentExclusionPolicies"] = from_union([lambda x: from_list(lambda x: to_class(OptionsUpdateAdditionalContentExclusionPolicy, x), x), from_none], self.additional_content_exclusion_policies) if self.agent_context is not None: result["agentContext"] = from_union([from_str, from_none], self.agent_context) + if self.allow_all_mcp_server_instructions is not None: + result["allowAllMcpServerInstructions"] = from_union([from_bool, from_none], self.allow_all_mcp_server_instructions) if self.ask_user_disabled is not None: result["askUserDisabled"] = from_union([from_bool, from_none], self.ask_user_disabled) if self.available_tools is not None: @@ -19815,6 +19915,8 @@ def to_dict(self) -> dict: result["reasoningEffort"] = from_union([from_str, from_none], self.reasoning_effort) if self.reasoning_summary is not None: result["reasoningSummary"] = from_union([lambda x: to_enum(ReasoningSummary, x), from_none], self.reasoning_summary) + if self.response_budget is not None: + result["responseBudget"] = from_union([lambda x: to_class(ResponseBudgetConfig, x), from_none], self.response_budget) if self.running_in_interactive_mode is not None: result["runningInInteractiveMode"] = from_union([from_bool, from_none], self.running_in_interactive_mode) if self.sandbox_config is not None: @@ -20847,12 +20949,21 @@ class SubagentSettings: disabled_subagents: list[str] | None = None """Names of subagents the user has turned off; they cannot be dispatched""" + max_concurrency: int | None = None + """Maximum number of subagents that can run concurrently; applies to usage-based billing + users only + """ + max_depth: int | None = None + """Maximum subagent nesting depth; applies to usage-based billing users only""" + @staticmethod def from_dict(obj: Any) -> 'SubagentSettings': assert isinstance(obj, dict) agents = from_union([lambda x: from_dict(SubagentSettingsEntry.from_dict, x), from_none], obj.get("agents")) disabled_subagents = from_union([lambda x: from_list(from_str, x), from_none], obj.get("disabledSubagents")) - return SubagentSettings(agents, disabled_subagents) + max_concurrency = from_union([from_int, from_none], obj.get("maxConcurrency")) + max_depth = from_union([from_int, from_none], obj.get("maxDepth")) + return SubagentSettings(agents, disabled_subagents, max_concurrency, max_depth) def to_dict(self) -> dict: result: dict = {} @@ -20860,6 +20971,10 @@ def to_dict(self) -> dict: result["agents"] = from_union([lambda x: from_dict(lambda x: to_class(SubagentSettingsEntry, x), x), from_none], self.agents) if self.disabled_subagents is not None: result["disabledSubagents"] = from_union([lambda x: from_list(from_str, x), from_none], self.disabled_subagents) + if self.max_concurrency is not None: + result["maxConcurrency"] = from_union([from_int, from_none], self.max_concurrency) + if self.max_depth is not None: + result["maxDepth"] = from_union([from_int, from_none], self.max_depth) return result # Experimental: this type is part of an experimental API and may change or be removed. @@ -21162,6 +21277,9 @@ class RPC: mcp_execute_sampling_request: dict[str, Any] mcp_execute_sampling_result: dict[str, Any] mcp_filtered_server: MCPFilteredServer + mcp_headers_handle_pending_headers_refresh_request: MCPHeadersHandlePendingHeadersRefreshRequest + mcp_headers_handle_pending_headers_refresh_request_request: MCPHeadersHandlePendingHeadersRefreshRequestRequest + mcp_headers_handle_pending_headers_refresh_request_result: MCPHeadersHandlePendingHeadersRefreshRequestResult mcp_host_state: MCPHostState mcp_is_server_running_request: MCPIsServerRunningRequest mcp_is_server_running_result: MCPIsServerRunningResult @@ -21934,6 +22052,9 @@ def from_dict(obj: Any) -> 'RPC': mcp_execute_sampling_request = from_dict(lambda x: x, obj.get("McpExecuteSamplingRequest")) mcp_execute_sampling_result = from_dict(lambda x: x, obj.get("McpExecuteSamplingResult")) mcp_filtered_server = MCPFilteredServer.from_dict(obj.get("McpFilteredServer")) + mcp_headers_handle_pending_headers_refresh_request = MCPHeadersHandlePendingHeadersRefreshRequest.from_dict(obj.get("McpHeadersHandlePendingHeadersRefreshRequest")) + mcp_headers_handle_pending_headers_refresh_request_request = MCPHeadersHandlePendingHeadersRefreshRequestRequest.from_dict(obj.get("McpHeadersHandlePendingHeadersRefreshRequestRequest")) + mcp_headers_handle_pending_headers_refresh_request_result = MCPHeadersHandlePendingHeadersRefreshRequestResult.from_dict(obj.get("McpHeadersHandlePendingHeadersRefreshRequestResult")) mcp_host_state = MCPHostState.from_dict(obj.get("McpHostState")) mcp_is_server_running_request = MCPIsServerRunningRequest.from_dict(obj.get("McpIsServerRunningRequest")) mcp_is_server_running_result = MCPIsServerRunningResult.from_dict(obj.get("McpIsServerRunningResult")) @@ -22481,7 +22602,7 @@ def from_dict(obj: Any) -> 'RPC': subagent_settings = from_union([SubagentSettings.from_dict, from_none], obj.get("SubagentSettings")) task_progress = from_union([TaskProgress.from_dict, from_none], obj.get("TaskProgress")) workspace_summary = from_union([WorkspaceSummary.from_dict, from_none], obj.get("WorkspaceSummary")) - return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agents_get_discovery_paths_request, allow_all_permission_set_result, allow_all_permission_state, api_key_auth_info, auth_info, auth_info_type, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_session_context, capi_session_options, command_list, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, configure_session_extensions_params, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_filter_mode, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, discovered_canvas, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_compact_context_window, history_compact_request, history_compact_result, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_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_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_filtered_server, mcp_host_state, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_register_external_client_request, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_restart_server_request, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_stdio, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_tools, mcp_unregister_external_client_request, memory_configuration, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_billing, model_billing_token_prices, model_billing_token_prices_long_context, 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_list_request, model_picker_category, model_picker_price_category, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, 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_reject, permission_decision_request, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_allow_all_request, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_allow_all_request, permissions_set_allow_all_source, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_list, plugin_list_result, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, poll_spawned_sessions_result, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, queued_command_handled, queued_command_not_handled, queued_command_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_most_recent_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, sandbox_config, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_seatbelt, schedule_entry, schedule_list, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_mode, send_request, send_result, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_auth_status, session_bulk_delete_result, session_capability, session_context, session_context_host_type, session_enrich_metadata_result, 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_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_poll_spawned_sessions_event, sessions_poll_spawned_sessions_request, sessions_prune_old_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_working_directory_context, session_working_directory_context_host_type, shell_cancel_user_requested_request, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_kill_request, shell_kill_result, shell_kill_signal, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_text_result, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, tool, tool_list, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, 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_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_diff_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspace_summary_host_type, workspaces_workspace_details_host_type, session_context_info, subagent_settings, task_progress, workspace_summary) + return RPC(abort_request, abort_result, account_all_users, account_get_all_users_result, account_get_current_auth_result, account_get_quota_request, account_get_quota_result, account_login_request, account_login_result, account_logout_request, account_logout_result, account_quota_snapshot, agent_discovery_path, agent_discovery_path_list, agent_discovery_path_scope, agent_get_current_result, agent_info, agent_info_source, agent_list, agent_registry_live_target_entry, agent_registry_live_target_entry_attention_kind, agent_registry_live_target_entry_kind, agent_registry_live_target_entry_last_terminal_event, agent_registry_live_target_entry_status, agent_registry_log_capture, agent_registry_log_capture_open_error_reason, agent_registry_spawn_error, agent_registry_spawn_permission_mode, agent_registry_spawn_registry_timeout, agent_registry_spawn_request, agent_registry_spawn_result, agent_registry_spawn_spawned, agent_registry_spawn_validation_error, agent_registry_spawn_validation_error_field, agent_registry_spawn_validation_error_reason, agent_reload_result, agents_discover_request, agent_select_request, agent_select_result, agents_get_discovery_paths_request, allow_all_permission_set_result, allow_all_permission_state, api_key_auth_info, auth_info, auth_info_type, cancel_user_requested_shell_command_result, canvas_action, canvas_action_invoke_request, canvas_action_invoke_result, canvas_close_request, canvas_host_context, canvas_host_context_capabilities, canvas_json_schema, canvas_list, canvas_list_open_result, canvas_open_request, canvas_provider_close_request, canvas_provider_invoke_action_request, canvas_provider_open_request, canvas_provider_open_result, canvas_session_context, capi_session_options, command_list, commands_handle_pending_command_request, commands_handle_pending_command_result, commands_invoke_request, commands_list_request, commands_respond_to_queued_command_request, commands_respond_to_queued_command_result, configure_session_extensions_params, connected_remote_session_metadata, connected_remote_session_metadata_kind, connected_remote_session_metadata_repository, connect_remote_session_params, connect_request, connect_result, content_filter_mode, copilot_api_token_auth_info, copilot_user_response, copilot_user_response_endpoints, copilot_user_response_quota_snapshots, copilot_user_response_quota_snapshots_chat, copilot_user_response_quota_snapshots_completions, copilot_user_response_quota_snapshots_premium_interactions, current_model, current_tool_metadata, discovered_canvas, discovered_mcp_server, discovered_mcp_server_type, enqueue_command_params, enqueue_command_result, env_auth_info, event_log_read_request, event_log_release_interest_result, event_log_tail_result, event_log_types, events_agent_scope, events_cursor_status, events_read_result, execute_command_params, execute_command_result, extension, extension_context_push_input, extension_list, extensions_disable_request, extensions_enable_request, extension_source, extension_status, external_tool_result, external_tool_text_result_for_llm, external_tool_text_result_for_llm_binary_results_for_llm, external_tool_text_result_for_llm_binary_results_for_llm_type, external_tool_text_result_for_llm_content, external_tool_text_result_for_llm_content_audio, external_tool_text_result_for_llm_content_image, external_tool_text_result_for_llm_content_resource, external_tool_text_result_for_llm_content_resource_details, external_tool_text_result_for_llm_content_resource_link, external_tool_text_result_for_llm_content_resource_link_icon, external_tool_text_result_for_llm_content_resource_link_icon_theme, external_tool_text_result_for_llm_content_terminal, external_tool_text_result_for_llm_content_text, filter_mapping, fleet_start_request, fleet_start_result, folder_trust_add_params, folder_trust_check_params, folder_trust_check_result, gh_cli_auth_info, handle_pending_tool_call_request, handle_pending_tool_call_result, history_abort_manual_compaction_result, history_cancel_background_compaction_result, history_compact_context_window, history_compact_request, history_compact_result, history_summarize_for_handoff_result, history_truncate_request, history_truncate_result, hmac_auth_info, installed_plugin, installed_plugin_info, installed_plugin_source, installed_plugin_source_git_hub, installed_plugin_source_local, installed_plugin_source_url, instruction_discovery_path, instruction_discovery_path_kind, instruction_discovery_path_list, instruction_discovery_path_location, instructions_discover_request, instructions_get_discovery_paths_request, instructions_get_sources_result, instruction_source, instruction_source_location, instruction_source_type, llm_inference_headers, llm_inference_http_request_chunk_request, llm_inference_http_request_chunk_result, llm_inference_http_request_start_request, llm_inference_http_request_start_result, llm_inference_http_request_start_transport, llm_inference_http_response_chunk_error, llm_inference_http_response_chunk_request, llm_inference_http_response_chunk_result, llm_inference_http_response_start_request, llm_inference_http_response_start_result, llm_inference_set_provider_result, local_session_metadata_value, log_request, log_result, lsp_initialize_request, marketplace_add_result, marketplace_browse_result, marketplace_info, marketplace_list_result, marketplace_plugin_info, marketplace_refresh_entry, marketplace_refresh_result, marketplace_remove_result, mcp_allowed_server, mcp_apps_call_tool_request, mcp_apps_diagnose_capability, mcp_apps_diagnose_request, mcp_apps_diagnose_result, mcp_apps_diagnose_server, mcp_apps_host_context, mcp_apps_host_context_details, mcp_apps_host_context_details_available_display_mode, mcp_apps_host_context_details_display_mode, mcp_apps_host_context_details_platform, mcp_apps_host_context_details_theme, mcp_apps_list_tools_request, mcp_apps_list_tools_result, mcp_apps_read_resource_request, mcp_apps_read_resource_result, mcp_apps_resource_content, mcp_apps_set_host_context_details, mcp_apps_set_host_context_details_available_display_mode, mcp_apps_set_host_context_details_display_mode, mcp_apps_set_host_context_details_platform, mcp_apps_set_host_context_details_theme, mcp_apps_set_host_context_request, mcp_cancel_sampling_execution_params, mcp_cancel_sampling_execution_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_configure_git_hub_request, mcp_configure_git_hub_result, mcp_disable_request, mcp_discover_request, mcp_discover_result, mcp_enable_request, mcp_execute_sampling_params, mcp_execute_sampling_request, mcp_execute_sampling_result, mcp_filtered_server, mcp_headers_handle_pending_headers_refresh_request, mcp_headers_handle_pending_headers_refresh_request_request, mcp_headers_handle_pending_headers_refresh_request_result, mcp_host_state, mcp_is_server_running_request, mcp_is_server_running_result, mcp_list_tools_request, mcp_list_tools_result, mcp_oauth_handle_pending_request, mcp_oauth_handle_pending_result, mcp_oauth_login_grant_type, mcp_oauth_login_request, mcp_oauth_login_result, mcp_oauth_pending_request_response, mcp_oauth_respond_request, mcp_oauth_respond_result, mcp_register_external_client_request, mcp_reload_with_config_request, mcp_remove_git_hub_result, mcp_restart_server_request, mcp_sampling_execution_action, mcp_sampling_execution_result, mcp_server, mcp_server_auth_config, mcp_server_auth_config_redirect_port, mcp_server_config, mcp_server_config_defer_tools, mcp_server_config_http, mcp_server_config_http_oauth_grant_type, mcp_server_config_http_type, mcp_server_config_stdio, mcp_server_failure_info, mcp_server_list, mcp_server_needs_auth_info, mcp_set_env_value_mode_details, mcp_set_env_value_mode_params, mcp_set_env_value_mode_result, mcp_start_server_request, mcp_start_servers_result, mcp_stop_server_request, mcp_tools, mcp_unregister_external_client_request, memory_configuration, metadata_context_info_request, metadata_context_info_result, metadata_is_processing_result, metadata_recompute_context_tokens_request, metadata_recompute_context_tokens_result, metadata_record_context_change_request, metadata_record_context_change_result, metadata_set_working_directory_request, metadata_set_working_directory_result, metadata_snapshot_current_mode, metadata_snapshot_remote_metadata, metadata_snapshot_remote_metadata_repository, metadata_snapshot_remote_metadata_task_type, model, model_billing, model_billing_token_prices, model_billing_token_prices_long_context, 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_list_request, model_picker_category, model_picker_price_category, model_policy, model_policy_state, model_set_reasoning_effort_request, model_set_reasoning_effort_result, models_list_request, model_switch_to_request, model_switch_to_result, mode_set_request, named_provider_config, name_get_result, name_set_auto_request, name_set_auto_result, name_set_request, open_canvas_instance, options_update_additional_content_exclusion_policy, options_update_additional_content_exclusion_policy_rule, options_update_additional_content_exclusion_policy_rule_source, options_update_additional_content_exclusion_policy_scope, options_update_context_tier, options_update_env_value_mode, options_update_reasoning_summary, options_update_tool_filter_precedence, pending_permission_request, pending_permission_request_list, permission_decision, permission_decision_approved, permission_decision_approved_for_location, permission_decision_approved_for_session, permission_decision_approve_for_location, permission_decision_approve_for_location_approval, permission_decision_approve_for_location_approval_commands, permission_decision_approve_for_location_approval_custom_tool, permission_decision_approve_for_location_approval_extension_management, permission_decision_approve_for_location_approval_extension_permission_access, permission_decision_approve_for_location_approval_mcp, permission_decision_approve_for_location_approval_mcp_sampling, permission_decision_approve_for_location_approval_memory, permission_decision_approve_for_location_approval_read, permission_decision_approve_for_location_approval_write, permission_decision_approve_for_session, permission_decision_approve_for_session_approval, permission_decision_approve_for_session_approval_commands, permission_decision_approve_for_session_approval_custom_tool, permission_decision_approve_for_session_approval_extension_management, permission_decision_approve_for_session_approval_extension_permission_access, permission_decision_approve_for_session_approval_mcp, permission_decision_approve_for_session_approval_mcp_sampling, permission_decision_approve_for_session_approval_memory, permission_decision_approve_for_session_approval_read, permission_decision_approve_for_session_approval_write, permission_decision_approve_once, permission_decision_approve_permanently, permission_decision_cancelled, 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_reject, permission_decision_request, permission_decision_user_not_available, permission_location_add_tool_approval_params, permission_location_apply_params, permission_location_apply_result, permission_location_resolve_params, permission_location_resolve_result, permission_location_type, permission_paths_add_params, permission_paths_allowed_check_params, permission_paths_allowed_check_result, permission_paths_config, permission_paths_list, permission_paths_update_primary_params, permission_paths_workspace_check_params, permission_paths_workspace_check_result, permission_prompt_shown_notification, permission_request_result, permission_rules_set, permissions_configure_additional_content_exclusion_policy, permissions_configure_additional_content_exclusion_policy_rule, permissions_configure_additional_content_exclusion_policy_rule_source, permissions_configure_additional_content_exclusion_policy_scope, permissions_configure_params, permissions_configure_result, permissions_folder_trust_add_trusted_result, permissions_get_allow_all_request, permissions_locations_add_tool_approval_details, permissions_locations_add_tool_approval_details_commands, permissions_locations_add_tool_approval_details_custom_tool, permissions_locations_add_tool_approval_details_extension_management, permissions_locations_add_tool_approval_details_extension_permission_access, permissions_locations_add_tool_approval_details_mcp, permissions_locations_add_tool_approval_details_mcp_sampling, permissions_locations_add_tool_approval_details_memory, permissions_locations_add_tool_approval_details_read, permissions_locations_add_tool_approval_details_write, permissions_locations_add_tool_approval_result, permissions_modify_rules_params, permissions_modify_rules_result, permissions_modify_rules_scope, permissions_notify_prompt_shown_result, permissions_paths_add_result, permissions_paths_list_request, permissions_paths_update_primary_result, permissions_pending_requests_request, permissions_reset_session_approvals_request, permissions_reset_session_approvals_result, permissions_set_allow_all_request, permissions_set_allow_all_source, permissions_set_approve_all_request, permissions_set_approve_all_result, permissions_set_approve_all_source, permissions_set_required_request, permissions_set_required_result, permissions_urls_set_unrestricted_mode_result, permission_urls_config, permission_urls_set_unrestricted_mode_params, ping_request, ping_result, plan_read_result, plan_read_sql_todos_result, plan_read_sql_todos_with_dependencies_result, plan_sql_todo_dependency, plan_sql_todos_row, plan_update_request, plugin, plugin_install_result, plugin_list, plugin_list_result, plugins_disable_request, plugins_enable_request, plugins_install_request, plugins_marketplaces_add_request, plugins_marketplaces_browse_request, plugins_marketplaces_refresh_request, plugins_marketplaces_remove_request, plugins_reload_request, plugins_uninstall_request, plugins_update_request, plugin_update_all_entry, plugin_update_all_result, plugin_update_result, poll_spawned_sessions_result, provider_add_request, provider_add_result, provider_config, provider_config_azure, provider_config_transport, provider_config_type, provider_config_wire_api, provider_endpoint, provider_endpoint_transport, provider_endpoint_type, provider_endpoint_wire_api, provider_get_endpoint_request, provider_model_config, provider_session_token, provider_token_acquire_request, provider_token_acquire_result, push_attachment, push_attachment_blob, push_attachment_directory, push_attachment_file, push_attachment_file_line_range, push_attachment_git_hub_reference, push_attachment_git_hub_reference_type, push_attachment_selection, push_attachment_selection_details, push_attachment_selection_details_end, push_attachment_selection_details_start, queued_command_handled, queued_command_not_handled, queued_command_result, queue_pending_items, queue_pending_items_kind, queue_pending_items_result, queue_remove_most_recent_result, register_event_interest_params, register_event_interest_result, register_extension_tools_params, register_extension_tools_result, release_event_interest_params, remote_control_config, remote_control_config_existing_mc_session, remote_control_status, remote_control_status_active, remote_control_status_connecting, remote_control_status_error, remote_control_status_off, remote_control_status_result, remote_control_stop_result, remote_control_transfer_result, remote_enable_request, remote_enable_result, remote_notify_steerable_changed_request, remote_notify_steerable_changed_result, remote_session_connection_result, remote_session_metadata_repository, remote_session_metadata_task_type, remote_session_metadata_value, remote_session_mode, remote_session_repository, sandbox_config, sandbox_config_user_policy, sandbox_config_user_policy_experimental, sandbox_config_user_policy_experimental_seatbelt, sandbox_config_user_policy_filesystem, sandbox_config_user_policy_network, sandbox_config_user_policy_seatbelt, schedule_entry, schedule_list, schedule_stop_request, schedule_stop_result, secrets_add_filter_values_request, secrets_add_filter_values_result, send_agent_mode, send_attachments_to_message_params, send_mode, send_request, send_result, server_agent_list, server_instruction_source_list, server_skill, server_skill_list, session_activity, session_auth_status, session_bulk_delete_result, session_capability, session_context, session_context_host_type, session_enrich_metadata_result, 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_capabilities, session_fs_set_provider_conventions, session_fs_set_provider_request, session_fs_set_provider_result, session_fs_sqlite_exists_request, session_fs_sqlite_exists_result, session_fs_sqlite_query_request, session_fs_sqlite_query_result, session_fs_sqlite_query_type, session_fs_stat_request, session_fs_stat_result, session_fs_write_file_request, session_installed_plugin, session_installed_plugin_source, session_installed_plugin_source_git_hub, session_installed_plugin_source_local, session_installed_plugin_source_url, session_list, session_list_entry, session_list_filter, session_load_deferred_repo_hooks_result, session_log_level, session_mcp_apps_call_tool_result, session_metadata_snapshot, session_mode, session_model_list, session_open_options, session_open_options_additional_content_exclusion_policy, session_open_options_additional_content_exclusion_policy_rule, session_open_options_additional_content_exclusion_policy_rule_source, session_open_options_additional_content_exclusion_policy_scope, session_open_options_env_value_mode, session_open_options_reasoning_summary, session_open_params, session_open_result, session_prune_result, sessions_bulk_delete_request, sessions_check_in_use_request, sessions_check_in_use_result, sessions_close_request, sessions_close_result, sessions_enrich_metadata_request, session_set_credentials_params, session_set_credentials_result, sessions_find_by_prefix_request, sessions_find_by_prefix_result, sessions_find_by_task_id_request, sessions_find_by_task_id_result, sessions_fork_request, sessions_fork_result, sessions_get_board_entry_count_request, sessions_get_board_entry_count_result, sessions_get_event_file_path_request, sessions_get_event_file_path_result, sessions_get_last_for_context_request, sessions_get_last_for_context_result, sessions_get_persisted_remote_steerable_request, sessions_get_persisted_remote_steerable_result, session_sizes, sessions_list_request, sessions_load_deferred_repo_hooks_request, sessions_open_attach, sessions_open_cloud, sessions_open_create, sessions_open_handoff, sessions_open_handoff_task_type, sessions_open_progress, sessions_open_progress_status, sessions_open_progress_step, sessions_open_remote, sessions_open_resume, sessions_open_resume_last, sessions_open_status, session_source, sessions_poll_spawned_sessions_event, sessions_poll_spawned_sessions_request, sessions_prune_old_request, sessions_register_extension_tools_on_session_options, sessions_release_lock_request, sessions_release_lock_result, sessions_reload_plugin_hooks_request, sessions_reload_plugin_hooks_result, sessions_save_request, sessions_save_result, sessions_set_additional_plugins_request, sessions_set_additional_plugins_result, sessions_set_remote_control_steering_request, sessions_start_remote_control_request, sessions_stop_remote_control_request, sessions_transfer_remote_control_request, session_telemetry_engagement, session_update_options_params, session_update_options_result, session_working_directory_context, session_working_directory_context_host_type, shell_cancel_user_requested_request, shell_exec_request, shell_exec_result, shell_execute_user_requested_request, shell_kill_request, shell_kill_result, shell_kill_signal, shutdown_request, skill, skill_discovery_path, skill_discovery_path_list, skill_discovery_scope, skill_list, skills_config_set_disabled_skills_request, skills_disable_request, skills_discover_request, skills_enable_request, skills_get_discovery_paths_request, skills_get_invoked_result, skills_invoked_skill, skills_load_diagnostics, slash_command_agent_prompt_result, slash_command_completed_result, slash_command_info, slash_command_input, slash_command_input_completion, slash_command_invocation_result, slash_command_kind, slash_command_select_subcommand_option, slash_command_select_subcommand_result, slash_command_text_result, subagent_settings_entry, subagent_settings_entry_context_tier, task_agent_info, task_agent_progress, task_execution_mode, task_info, task_list, task_progress_line, tasks_cancel_request, tasks_cancel_result, tasks_get_current_promotable_result, tasks_get_progress_request, tasks_get_progress_result, task_shell_info, task_shell_info_attachment_mode, task_shell_progress, tasks_promote_current_to_background_result, tasks_promote_to_background_request, tasks_promote_to_background_result, tasks_refresh_result, tasks_remove_request, tasks_remove_result, tasks_send_message_request, tasks_send_message_result, tasks_start_agent_request, tasks_start_agent_result, task_status, tasks_wait_for_pending_result, telemetry_set_feature_overrides_request, token_auth_info, tool, tool_list, tools_get_current_metadata_result, tools_initialize_and_validate_result, tools_list_request, tools_update_subagent_settings_result, ui_auto_mode_switch_response, 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_ephemeral_query_request, ui_ephemeral_query_result, ui_exit_plan_mode_action, ui_exit_plan_mode_response, ui_handle_pending_auto_mode_switch_request, ui_handle_pending_elicitation_request, ui_handle_pending_exit_plan_mode_request, ui_handle_pending_result, ui_handle_pending_sampling_request, ui_handle_pending_sampling_response, ui_handle_pending_user_input_request, ui_register_direct_auto_mode_switch_handler_result, ui_unregister_direct_auto_mode_switch_handler_request, ui_unregister_direct_auto_mode_switch_handler_result, ui_user_input_response, update_subagent_settings_request, usage_get_metrics_result, usage_metrics_code_changes, usage_metrics_model_metric, usage_metrics_model_metric_requests, usage_metrics_model_metric_token_detail, usage_metrics_model_metric_usage, usage_metrics_token_detail, user_auth_info, user_requested_shell_command_result, workspace_diff_file_change, workspace_diff_file_change_type, workspace_diff_mode, workspace_diff_result, workspaces_checkpoints, workspaces_create_file_request, workspaces_diff_request, workspaces_get_workspace_result, workspaces_list_checkpoints_result, workspaces_list_files_result, workspaces_read_checkpoint_request, workspaces_read_checkpoint_result, workspaces_read_file_request, workspaces_read_file_result, workspaces_save_large_paste_request, workspaces_save_large_paste_result, workspace_summary_host_type, workspaces_workspace_details_host_type, session_context_info, subagent_settings, task_progress, workspace_summary) def to_dict(self) -> dict: result: dict = {} @@ -22706,6 +22827,9 @@ def to_dict(self) -> dict: result["McpExecuteSamplingRequest"] = from_dict(lambda x: x, self.mcp_execute_sampling_request) result["McpExecuteSamplingResult"] = from_dict(lambda x: x, self.mcp_execute_sampling_result) result["McpFilteredServer"] = to_class(MCPFilteredServer, self.mcp_filtered_server) + result["McpHeadersHandlePendingHeadersRefreshRequest"] = to_class(MCPHeadersHandlePendingHeadersRefreshRequest, self.mcp_headers_handle_pending_headers_refresh_request) + result["McpHeadersHandlePendingHeadersRefreshRequestRequest"] = to_class(MCPHeadersHandlePendingHeadersRefreshRequestRequest, self.mcp_headers_handle_pending_headers_refresh_request_request) + result["McpHeadersHandlePendingHeadersRefreshRequestResult"] = to_class(MCPHeadersHandlePendingHeadersRefreshRequestResult, self.mcp_headers_handle_pending_headers_refresh_request_result) result["McpHostState"] = to_class(MCPHostState, self.mcp_host_state) result["McpIsServerRunningRequest"] = to_class(MCPIsServerRunningRequest, self.mcp_is_server_running_request) result["McpIsServerRunningResult"] = to_class(MCPIsServerRunningResult, self.mcp_is_server_running_result) @@ -23449,7 +23573,7 @@ def _load_SessionOpenParams(obj: Any) -> "SessionOpenParams": case "handoff": return SessionsOpenHandoff.from_dict(obj) case _: raise ValueError(f"Unknown SessionOpenParams kind: {kind!r}") -# Result of invoking the slash command (text output, prompt to send to the agent, or completion). +# Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). SlashCommandInvocationResult = SlashCommandTextResult | SlashCommandAgentPromptResult | SlashCommandCompletedResult | SlashCommandSelectSubcommandResult def _load_SlashCommandInvocationResult(obj: Any) -> "SlashCommandInvocationResult": @@ -24418,6 +24542,19 @@ async def login(self, params: MCPOauthLoginRequest, *, timeout: float | None = N 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 McpHeadersApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def handle_pending_headers_refresh_request(self, params: MCPHeadersHandlePendingHeadersRefreshRequestRequest, *, timeout: float | None = None) -> MCPHeadersHandlePendingHeadersRefreshRequestResult: + "Responds to a pending MCP dynamic headers refresh request. Hosts that subscribe to `mcp.headers_refresh_required` use this to provide short-lived per-server headers or to indicate that no dynamic headers are available for this refresh.\n\nArgs:\n params: MCP headers refresh request id and the host response.\n\nReturns:\n Indicates whether the pending MCP headers refresh response was accepted." + 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 MCPHeadersHandlePendingHeadersRefreshRequestResult.from_dict(await self._client.request("session.mcp.headers.handlePendingHeadersRefreshRequest", params_dict, **_timeout_kwargs(timeout))) + + # Experimental: this API group is experimental and may change or be removed. class McpAppsApi: def __init__(self, client: "JsonRpcClient", session_id: str): @@ -24465,6 +24602,7 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self._client = client self._session_id = session_id self.oauth = McpOauthApi(client, session_id) + self.headers = McpHeadersApi(client, session_id) self.apps = McpAppsApi(client, session_id) async def list(self, *, timeout: float | None = None) -> MCPServerList: @@ -24663,7 +24801,7 @@ async def list(self, params: CommandsListRequest | None = None, *, timeout: floa return CommandList.from_dict(await self._client.request("session.commands.list", params_dict, **_timeout_kwargs(timeout))) async def invoke(self, params: CommandsInvokeRequest, *, timeout: float | None = None) -> SlashCommandInvocationResult: - "Invokes a slash command in the session.\n\nArgs:\n params: Slash command name and optional raw input string to invoke.\n\nReturns:\n Result of invoking the slash command (text output, prompt to send to the agent, or completion)." + "Invokes a slash command in the session.\n\nArgs:\n params: Slash command name and optional raw input string to invoke.\n\nReturns:\n Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection)." 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 _load_SlashCommandInvocationResult(await self._client.request("session.commands.invoke", params_dict, **_timeout_kwargs(timeout))) @@ -25723,6 +25861,10 @@ async def handle_llm_inference_http_request_chunk(params: dict) -> dict | None: "MCPExecuteSamplingParams", "MCPFilteredServer", "MCPGrantType", + "MCPHeadersHandlePendingHeadersRefreshRequest", + "MCPHeadersHandlePendingHeadersRefreshRequestKind", + "MCPHeadersHandlePendingHeadersRefreshRequestRequest", + "MCPHeadersHandlePendingHeadersRefreshRequestResult", "MCPHostState", "MCPIsServerRunningRequest", "MCPIsServerRunningResult", @@ -25779,6 +25921,7 @@ async def handle_llm_inference_http_request_chunk(params: dict) -> dict | None: "McpAppsSetHostContextDetailsTheme", "McpExecuteSamplingRequest", "McpExecuteSamplingResult", + "McpHeadersApi", "McpOauthApi", "McpOauthLoginGrantType", "McpServerAuthConfig", diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 0a3666c094..0ef5dccbc7 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -160,6 +160,7 @@ class SessionEventType(Enum): ASSISTANT_MESSAGE_START = "assistant.message_start" ASSISTANT_MESSAGE_DELTA = "assistant.message_delta" ASSISTANT_TURN_END = "assistant.turn_end" + ASSISTANT_IDLE = "assistant.idle" ASSISTANT_USAGE = "assistant.usage" MODEL_CALL_FAILURE = "model.call_failure" ABORT = "abort" @@ -191,6 +192,8 @@ class SessionEventType(Enum): SAMPLING_COMPLETED = "sampling.completed" MCP_OAUTH_REQUIRED = "mcp.oauth_required" MCP_OAUTH_COMPLETED = "mcp.oauth_completed" + MCP_HEADERS_REFRESH_REQUIRED = "mcp.headers_refresh_required" + MCP_HEADERS_REFRESH_COMPLETED = "mcp.headers_refresh_completed" SESSION_CUSTOM_NOTIFICATION = "session.custom_notification" EXTERNAL_TOOL_REQUESTED = "external_tool.requested" EXTERNAL_TOOL_COMPLETED = "external_tool.completed" @@ -957,6 +960,26 @@ def to_dict(self) -> dict: return result +@dataclass +class AssistantIdleData: + "Payload emitted whenever the main agent's processing loop goes idle, including while related background work (running agents or in-flight attached shell commands) is still pending and the session-level idle event is therefore deferred" + aborted: bool | None = None + + @staticmethod + def from_dict(obj: Any) -> "AssistantIdleData": + assert isinstance(obj, dict) + aborted = from_union([from_none, from_bool], obj.get("aborted")) + return AssistantIdleData( + aborted=aborted, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.aborted is not None: + result["aborted"] = from_union([from_none, from_bool], self.aborted) + return result + + @dataclass class AssistantIntentData: "Agent intent description for current activity or plan" @@ -2849,6 +2872,60 @@ def to_dict(self) -> dict: return result +@dataclass +class McpHeadersRefreshCompletedData: + "MCP headers refresh request completion notification" + outcome: McpHeadersRefreshCompletedOutcome + request_id: str + + @staticmethod + def from_dict(obj: Any) -> "McpHeadersRefreshCompletedData": + assert isinstance(obj, dict) + outcome = parse_enum(McpHeadersRefreshCompletedOutcome, obj.get("outcome")) + request_id = from_str(obj.get("requestId")) + return McpHeadersRefreshCompletedData( + outcome=outcome, + request_id=request_id, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["outcome"] = to_enum(McpHeadersRefreshCompletedOutcome, self.outcome) + result["requestId"] = from_str(self.request_id) + return result + + +@dataclass +class McpHeadersRefreshRequiredData: + "Dynamic headers refresh request for a remote MCP server" + reason: McpHeadersRefreshRequiredReason + request_id: str + server_name: str + server_url: str + + @staticmethod + def from_dict(obj: Any) -> "McpHeadersRefreshRequiredData": + assert isinstance(obj, dict) + reason = parse_enum(McpHeadersRefreshRequiredReason, obj.get("reason")) + request_id = from_str(obj.get("requestId")) + server_name = from_str(obj.get("serverName")) + server_url = from_str(obj.get("serverUrl")) + return McpHeadersRefreshRequiredData( + reason=reason, + request_id=request_id, + server_name=server_name, + server_url=server_url, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["reason"] = to_enum(McpHeadersRefreshRequiredReason, self.reason) + result["requestId"] = from_str(self.request_id) + result["serverName"] = from_str(self.server_name) + result["serverUrl"] = from_str(self.server_url) + return result + + @dataclass class McpOauthCompletedData: "MCP OAuth request completion notification" @@ -2875,6 +2952,7 @@ def to_dict(self) -> dict: @dataclass class McpOauthRequiredData: "OAuth authentication request for an MCP server" + reason: McpOauthRequestReason request_id: str server_name: str server_url: str @@ -2885,6 +2963,7 @@ class McpOauthRequiredData: @staticmethod def from_dict(obj: Any) -> "McpOauthRequiredData": assert isinstance(obj, dict) + reason = parse_enum(McpOauthRequestReason, obj.get("reason")) request_id = from_str(obj.get("requestId")) server_name = from_str(obj.get("serverName")) server_url = from_str(obj.get("serverUrl")) @@ -2892,6 +2971,7 @@ def from_dict(obj: Any) -> "McpOauthRequiredData": static_client_config = from_union([from_none, McpOauthRequiredStaticClientConfig.from_dict], obj.get("staticClientConfig")) www_authenticate_params = from_union([from_none, McpOauthWWWAuthenticateParams.from_dict], obj.get("wwwAuthenticateParams")) return McpOauthRequiredData( + reason=reason, request_id=request_id, server_name=server_name, server_url=server_url, @@ -2902,6 +2982,7 @@ def from_dict(obj: Any) -> "McpOauthRequiredData": def to_dict(self) -> dict: result: dict = {} + result["reason"] = to_enum(McpOauthRequestReason, self.reason) result["requestId"] = from_str(self.request_id) result["serverName"] = from_str(self.server_name) result["serverUrl"] = from_str(self.server_url) @@ -2918,6 +2999,7 @@ def to_dict(self) -> dict: class McpOauthRequiredStaticClientConfig: "Static OAuth client configuration, if the server specifies one" client_id: str + client_secret: str | None = None grant_type: str | None = None public_client: bool | None = None @@ -2925,10 +3007,12 @@ class McpOauthRequiredStaticClientConfig: def from_dict(obj: Any) -> "McpOauthRequiredStaticClientConfig": assert isinstance(obj, dict) client_id = from_str(obj.get("clientId")) + client_secret = from_union([from_none, from_str], obj.get("clientSecret")) grant_type = from_union([from_none, from_str], obj.get("grantType")) public_client = from_union([from_none, from_bool], obj.get("publicClient")) return McpOauthRequiredStaticClientConfig( client_id=client_id, + client_secret=client_secret, grant_type=grant_type, public_client=public_client, ) @@ -2936,6 +3020,8 @@ def from_dict(obj: Any) -> "McpOauthRequiredStaticClientConfig": def to_dict(self) -> dict: result: dict = {} result["clientId"] = from_str(self.client_id) + if self.client_secret is not None: + result["clientSecret"] = from_union([from_none, from_str], self.client_secret) if self.grant_type is not None: result["grantType"] = from_union([from_none, from_str], self.grant_type) if self.public_client is not None: @@ -2946,27 +3032,28 @@ def to_dict(self) -> dict: @dataclass class McpOauthWWWAuthenticateParams: "OAuth WWW-Authenticate parameters parsed from an MCP auth challenge" - resource_metadata_url: str error: str | None = None + resource_metadata_url: str | None = None scope: str | None = None @staticmethod def from_dict(obj: Any) -> "McpOauthWWWAuthenticateParams": assert isinstance(obj, dict) - resource_metadata_url = from_str(obj.get("resourceMetadataUrl")) error = from_union([from_none, from_str], obj.get("error")) + resource_metadata_url = from_union([from_none, from_str], obj.get("resourceMetadataUrl")) scope = from_union([from_none, from_str], obj.get("scope")) return McpOauthWWWAuthenticateParams( - resource_metadata_url=resource_metadata_url, error=error, + resource_metadata_url=resource_metadata_url, scope=scope, ) def to_dict(self) -> dict: result: dict = {} - result["resourceMetadataUrl"] = from_str(self.resource_metadata_url) if self.error is not None: result["error"] = from_union([from_none, from_str], self.error) + if self.resource_metadata_url is not None: + result["resourceMetadataUrl"] = from_union([from_none, from_str], self.resource_metadata_url) if self.scope is not None: result["scope"] = from_union([from_none, from_str], self.scope) return result @@ -4318,6 +4405,31 @@ def to_dict(self) -> dict: return result +@dataclass +class ResponseBudgetConfig: + "Optional response budget limits." + max_ai_credits: float | None = None + max_model_iterations: int | None = None + + @staticmethod + def from_dict(obj: Any) -> "ResponseBudgetConfig": + assert isinstance(obj, dict) + max_ai_credits = from_union([from_none, from_float], obj.get("maxAiCredits")) + max_model_iterations = from_union([from_none, from_int], obj.get("maxModelIterations")) + return ResponseBudgetConfig( + max_ai_credits=max_ai_credits, + max_model_iterations=max_model_iterations, + ) + + def to_dict(self) -> dict: + result: dict = {} + if self.max_ai_credits is not None: + result["maxAiCredits"] = from_union([from_none, to_float], self.max_ai_credits) + if self.max_model_iterations is not None: + result["maxModelIterations"] = from_union([from_none, to_int], self.max_model_iterations) + return result + + @dataclass class SamplingCompletedData: "Sampling request completion notification signaling UI dismissal" @@ -5097,6 +5209,7 @@ class SessionResumeData: reasoning_effort: str | None = None reasoning_summary: ReasoningSummary | None = None remote_steerable: bool | None = None + response_budget: ResponseBudgetConfig | None = None selected_model: str | None = None session_was_active: bool | None = None @@ -5113,6 +5226,7 @@ def from_dict(obj: Any) -> "SessionResumeData": reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("reasoningSummary")) remote_steerable = from_union([from_none, from_bool], obj.get("remoteSteerable")) + response_budget = from_union([from_none, ResponseBudgetConfig.from_dict], obj.get("responseBudget")) selected_model = from_union([from_none, from_str], obj.get("selectedModel")) session_was_active = from_union([from_none, from_bool], obj.get("sessionWasActive")) return SessionResumeData( @@ -5126,6 +5240,7 @@ def from_dict(obj: Any) -> "SessionResumeData": reasoning_effort=reasoning_effort, reasoning_summary=reasoning_summary, remote_steerable=remote_steerable, + response_budget=response_budget, selected_model=selected_model, session_was_active=session_was_active, ) @@ -5150,6 +5265,8 @@ def to_dict(self) -> dict: result["reasoningSummary"] = from_union([from_none, lambda x: to_enum(ReasoningSummary, x)], self.reasoning_summary) if self.remote_steerable is not None: result["remoteSteerable"] = from_union([from_none, from_bool], self.remote_steerable) + if self.response_budget is not None: + result["responseBudget"] = from_union([from_none, lambda x: to_class(ResponseBudgetConfig, x)], self.response_budget) if self.selected_model is not None: result["selectedModel"] = from_union([from_none, from_str], self.selected_model) if self.session_was_active is not None: @@ -5401,6 +5518,7 @@ class SessionStartData: reasoning_effort: str | None = None reasoning_summary: ReasoningSummary | None = None remote_steerable: bool | None = None + response_budget: ResponseBudgetConfig | None = None selected_model: str | None = None @staticmethod @@ -5418,6 +5536,7 @@ def from_dict(obj: Any) -> "SessionStartData": reasoning_effort = from_union([from_none, from_str], obj.get("reasoningEffort")) reasoning_summary = from_union([from_none, lambda x: parse_enum(ReasoningSummary, x)], obj.get("reasoningSummary")) remote_steerable = from_union([from_none, from_bool], obj.get("remoteSteerable")) + response_budget = from_union([from_none, ResponseBudgetConfig.from_dict], obj.get("responseBudget")) selected_model = from_union([from_none, from_str], obj.get("selectedModel")) return SessionStartData( copilot_version=copilot_version, @@ -5432,6 +5551,7 @@ def from_dict(obj: Any) -> "SessionStartData": reasoning_effort=reasoning_effort, reasoning_summary=reasoning_summary, remote_steerable=remote_steerable, + response_budget=response_budget, selected_model=selected_model, ) @@ -5456,6 +5576,8 @@ def to_dict(self) -> dict: result["reasoningSummary"] = from_union([from_none, lambda x: to_enum(ReasoningSummary, x)], self.reasoning_summary) if self.remote_steerable is not None: result["remoteSteerable"] = from_union([from_none, from_bool], self.remote_steerable) + if self.response_budget is not None: + result["responseBudget"] = from_union([from_none, lambda x: to_class(ResponseBudgetConfig, x)], self.response_budget) if self.selected_model is not None: result["selectedModel"] = from_union([from_none, from_str], self.selected_model) return result @@ -7091,6 +7213,7 @@ class ToolExecutionStartData: model: str | None = None # Deprecated: this field is deprecated. parent_tool_call_id: str | None = None + shell_tool_info: ToolExecutionStartShellToolInfo | None = None tool_description: ToolExecutionStartToolDescription | None = None turn_id: str | None = None @@ -7105,6 +7228,7 @@ def from_dict(obj: Any) -> "ToolExecutionStartData": mcp_tool_name = from_union([from_none, from_str], obj.get("mcpToolName")) model = from_union([from_none, from_str], obj.get("model")) parent_tool_call_id = from_union([from_none, from_str], obj.get("parentToolCallId")) + shell_tool_info = from_union([from_none, ToolExecutionStartShellToolInfo.from_dict], obj.get("shellToolInfo")) tool_description = from_union([from_none, ToolExecutionStartToolDescription.from_dict], obj.get("toolDescription")) turn_id = from_union([from_none, from_str], obj.get("turnId")) return ToolExecutionStartData( @@ -7116,6 +7240,7 @@ def from_dict(obj: Any) -> "ToolExecutionStartData": mcp_tool_name=mcp_tool_name, model=model, parent_tool_call_id=parent_tool_call_id, + shell_tool_info=shell_tool_info, tool_description=tool_description, turn_id=turn_id, ) @@ -7136,6 +7261,8 @@ def to_dict(self) -> dict: result["model"] = from_union([from_none, from_str], self.model) if self.parent_tool_call_id is not None: result["parentToolCallId"] = from_union([from_none, from_str], self.parent_tool_call_id) + if self.shell_tool_info is not None: + result["shellToolInfo"] = from_union([from_none, lambda x: to_class(ToolExecutionStartShellToolInfo, x)], self.shell_tool_info) if self.tool_description is not None: result["toolDescription"] = from_union([from_none, lambda x: to_class(ToolExecutionStartToolDescription, x)], self.tool_description) if self.turn_id is not None: @@ -7143,6 +7270,29 @@ def to_dict(self) -> dict: return result +@dataclass +class ToolExecutionStartShellToolInfo: + "Shell-aware path hints for a shell tool's command, captured at start time so consumers can snapshot a file's pre-image before the tool runs." + has_write_file_redirection: bool + possible_paths: list[str] + + @staticmethod + def from_dict(obj: Any) -> "ToolExecutionStartShellToolInfo": + assert isinstance(obj, dict) + has_write_file_redirection = from_bool(obj.get("hasWriteFileRedirection")) + possible_paths = from_list(from_str, obj.get("possiblePaths")) + return ToolExecutionStartShellToolInfo( + has_write_file_redirection=has_write_file_redirection, + possible_paths=possible_paths, + ) + + def to_dict(self) -> dict: + result: dict = {} + result["hasWriteFileRedirection"] = from_bool(self.has_write_file_redirection) + result["possiblePaths"] = from_list(from_str, self.possible_paths) + return result + + @dataclass class ToolExecutionStartToolDescription: "Tool definition metadata, present for MCP tools with MCP Apps support" @@ -7318,6 +7468,7 @@ class UserMessageData: content: str agent_mode: UserMessageAgentMode | None = None attachments: list[Attachment] | None = None + delivery: UserMessageDelivery | None = None interaction_id: str | None = None is_autopilot_continuation: bool | None = None native_document_path_fallback_paths: list[str] | None = None @@ -7332,6 +7483,7 @@ def from_dict(obj: Any) -> "UserMessageData": content = from_str(obj.get("content")) agent_mode = from_union([from_none, lambda x: parse_enum(UserMessageAgentMode, x)], obj.get("agentMode")) attachments = from_union([from_none, lambda x: from_list(_load_Attachment, x)], obj.get("attachments")) + delivery = from_union([from_none, lambda x: parse_enum(UserMessageDelivery, x)], obj.get("delivery")) interaction_id = from_union([from_none, from_str], obj.get("interactionId")) is_autopilot_continuation = from_union([from_none, from_bool], obj.get("isAutopilotContinuation")) native_document_path_fallback_paths = from_union([from_none, lambda x: from_list(from_str, x)], obj.get("nativeDocumentPathFallbackPaths")) @@ -7343,6 +7495,7 @@ def from_dict(obj: Any) -> "UserMessageData": content=content, agent_mode=agent_mode, attachments=attachments, + delivery=delivery, interaction_id=interaction_id, is_autopilot_continuation=is_autopilot_continuation, native_document_path_fallback_paths=native_document_path_fallback_paths, @@ -7359,6 +7512,8 @@ def to_dict(self) -> dict: result["agentMode"] = from_union([from_none, lambda x: to_enum(UserMessageAgentMode, x)], self.agent_mode) if self.attachments is not None: result["attachments"] = from_union([from_none, lambda x: from_list(lambda x: x.to_dict(), x)], self.attachments) + if self.delivery is not None: + result["delivery"] = from_union([from_none, lambda x: to_enum(UserMessageDelivery, x)], self.delivery) if self.interaction_id is not None: result["interactionId"] = from_union([from_none, from_str], self.interaction_id) if self.is_autopilot_continuation is not None: @@ -7915,6 +8070,26 @@ class HandoffSourceType(Enum): LOCAL = "local" +class McpHeadersRefreshCompletedOutcome(Enum): + "How the pending MCP headers refresh request resolved." + # The host supplied dynamic headers. + HEADERS = "headers" + # The host responded with no dynamic headers. + NONE = "none" + # No response arrived within the bounded window. + TIMEOUT = "timeout" + + +class McpHeadersRefreshRequiredReason(Enum): + "Why dynamic headers are being requested." + # The transport is making its first dynamic header request for this server. + STARTUP = "startup" + # The previously cached dynamic headers expired. + TTL_EXPIRED = "ttl-expired" + # The server returned 401 and stale dynamic headers were invalidated. + AUTH_FAILED = "auth-failed" + + class McpOauthCompletionOutcome(Enum): "How the pending MCP OAuth request was completed" # The request completed with a token-backed OAuth provider. @@ -7923,6 +8098,18 @@ class McpOauthCompletionOutcome(Enum): CANCELLED = "cancelled" +class McpOauthRequestReason(Enum): + "Reason the runtime is requesting host-provided MCP OAuth credentials" + # Initial credentials are required before connecting to the MCP server. + INITIAL = "initial" + # The current host-provided credential was rejected and a replacement is requested. + REFRESH = "refresh" + # The server requires a new host authorization flow before continuing. + REAUTH = "reauth" + # The server requires a credential with additional scope or audience. + UPSCOPE = "upscope" + + class McpServerSource(Enum): "Configuration source: user, workspace, plugin, or builtin" # Server configured in the user's global MCP configuration. @@ -8149,6 +8336,16 @@ class UserMessageAgentMode(Enum): SHELL = "shell" +class UserMessageDelivery(Enum): + "How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn." + # Delivered while the loop was idle; starts its own run immediately (a human's fresh turn, or a system notification waking an idle agent). + IDLE = "idle" + # Injected into the current in-flight run while the agent was busy (immediate mode). + STEERING = "steering" + # Enqueued while the agent was busy; processed as its own run afterward. + QUEUED = "queued" + + class WorkingDirectoryContextHostType(Enum): "Hosting platform type of the repository (github or ado)" # Repository is hosted on GitHub. @@ -8165,7 +8362,7 @@ class WorkspaceFileChangedOperation(Enum): UPDATE = "update" -SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantUsageData | ModelCallFailureData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | SessionCustomNotificationData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data +SessionEventData = SessionStartData | SessionResumeData | SessionRemoteSteerableChangedData | SessionErrorData | SessionIdleData | SessionTitleChangedData | SessionScheduleCreatedData | SessionScheduleCancelledData | SessionScheduleRearmedData | SessionAutopilotObjectiveChangedData | SessionInfoData | SessionWarningData | SessionModelChangeData | SessionModeChangedData | SessionPermissionsChangedData | SessionPlanChangedData | SessionTodosChangedData | SessionWorkspaceFileChangedData | SessionHandoffData | SessionTruncationData | SessionSnapshotRewindData | SessionShutdownData | SessionContextChangedData | SessionUsageInfoData | SessionCompactionStartData | SessionCompactionCompleteData | SessionTaskCompleteData | UserMessageData | PendingMessagesModifiedData | AssistantTurnStartData | AssistantIntentData | AssistantReasoningData | AssistantReasoningDeltaData | AssistantStreamingDeltaData | AssistantMessageData | AssistantMessageStartData | AssistantMessageDeltaData | AssistantTurnEndData | AssistantIdleData | AssistantUsageData | ModelCallFailureData | AbortData | ToolUserRequestedData | ToolExecutionStartData | ToolExecutionPartialResultData | ToolExecutionProgressData | ToolExecutionCompleteData | SkillInvokedData | SubagentStartedData | SubagentCompletedData | SubagentFailedData | SubagentSelectedData | SubagentDeselectedData | HookStartData | HookEndData | HookProgressData | SessionBinaryAssetData | SystemMessageData | SystemNotificationData | PermissionRequestedData | PermissionCompletedData | UserInputRequestedData | UserInputCompletedData | ElicitationRequestedData | ElicitationCompletedData | SamplingRequestedData | SamplingCompletedData | McpOauthRequiredData | McpOauthCompletedData | McpHeadersRefreshRequiredData | McpHeadersRefreshCompletedData | SessionCustomNotificationData | ExternalToolRequestedData | ExternalToolCompletedData | CommandQueuedData | CommandExecuteData | CommandCompletedData | AutoModeSwitchRequestedData | AutoModeSwitchCompletedData | CommandsChangedData | CapabilitiesChangedData | ExitPlanModeRequestedData | ExitPlanModeCompletedData | SessionToolsUpdatedData | SessionBackgroundTasksChangedData | SessionSkillsLoadedData | SessionCustomAgentsUpdatedData | SessionMcpServersLoadedData | SessionMcpServerStatusChangedData | SessionExtensionsLoadedData | SessionCanvasOpenedData | SessionCanvasRegistryChangedData | SessionCanvasClosedData | SessionCanvasUnavailableData | SessionCanvasRecordedData | SessionCanvasRemovedData | SessionExtensionsAttachmentsPushedData | McpAppToolCallCompleteData | RawSessionEventData | Data @dataclass @@ -8229,6 +8426,7 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.ASSISTANT_MESSAGE_START: data = AssistantMessageStartData.from_dict(data_obj) case SessionEventType.ASSISTANT_MESSAGE_DELTA: data = AssistantMessageDeltaData.from_dict(data_obj) case SessionEventType.ASSISTANT_TURN_END: data = AssistantTurnEndData.from_dict(data_obj) + case SessionEventType.ASSISTANT_IDLE: data = AssistantIdleData.from_dict(data_obj) case SessionEventType.ASSISTANT_USAGE: data = AssistantUsageData.from_dict(data_obj) case SessionEventType.MODEL_CALL_FAILURE: data = ModelCallFailureData.from_dict(data_obj) case SessionEventType.ABORT: data = AbortData.from_dict(data_obj) @@ -8259,6 +8457,8 @@ def from_dict(obj: Any) -> "SessionEvent": case SessionEventType.SAMPLING_COMPLETED: data = SamplingCompletedData.from_dict(data_obj) case SessionEventType.MCP_OAUTH_REQUIRED: data = McpOauthRequiredData.from_dict(data_obj) case SessionEventType.MCP_OAUTH_COMPLETED: data = McpOauthCompletedData.from_dict(data_obj) + case SessionEventType.MCP_HEADERS_REFRESH_REQUIRED: data = McpHeadersRefreshRequiredData.from_dict(data_obj) + case SessionEventType.MCP_HEADERS_REFRESH_COMPLETED: data = McpHeadersRefreshCompletedData.from_dict(data_obj) case SessionEventType.SESSION_CUSTOM_NOTIFICATION: data = SessionCustomNotificationData.from_dict(data_obj) case SessionEventType.EXTERNAL_TOOL_REQUESTED: data = ExternalToolRequestedData.from_dict(data_obj) case SessionEventType.EXTERNAL_TOOL_COMPLETED: data = ExternalToolCompletedData.from_dict(data_obj) @@ -8322,6 +8522,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: __all__ = [ "AbortData", "AbortReason", + "AssistantIdleData", "AssistantIntentData", "AssistantMessageData", "AssistantMessageDeltaData", @@ -8407,8 +8608,13 @@ def session_event_to_dict(x: SessionEvent) -> Any: "McpAppToolCallCompleteError", "McpAppToolCallCompleteToolMeta", "McpAppToolCallCompleteToolMetaUI", + "McpHeadersRefreshCompletedData", + "McpHeadersRefreshCompletedOutcome", + "McpHeadersRefreshRequiredData", + "McpHeadersRefreshRequiredReason", "McpOauthCompletedData", "McpOauthCompletionOutcome", + "McpOauthRequestReason", "McpOauthRequiredData", "McpOauthRequiredStaticClientConfig", "McpOauthWWWAuthenticateParams", @@ -8471,6 +8677,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "PlanChangedOperation", "RawSessionEventData", "ReasoningSummary", + "ResponseBudgetConfig", "SamplingCompletedData", "SamplingRequestedData", "SessionAutopilotObjectiveChangedData", @@ -8577,6 +8784,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "ToolExecutionPartialResultData", "ToolExecutionProgressData", "ToolExecutionStartData", + "ToolExecutionStartShellToolInfo", "ToolExecutionStartToolDescription", "ToolExecutionStartToolDescriptionMeta", "ToolExecutionStartToolDescriptionMetaUI", @@ -8586,6 +8794,7 @@ def session_event_to_dict(x: SessionEvent) -> Any: "UserInputRequestedData", "UserMessageAgentMode", "UserMessageData", + "UserMessageDelivery", "UserToolSessionApproval", "UserToolSessionApprovalCommands", "UserToolSessionApprovalCustomTool", diff --git a/python/copilot/session.py b/python/copilot/session.py index 0dc569f258..4cedce9f5c 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -39,6 +39,9 @@ ExternalToolTextResultForLlm, HandlePendingToolCallRequest, LogRequest, + MCPOauthHandlePendingRequest, + MCPOauthPendingRequestResponse, + MCPOauthPendingRequestResponseKind, ModelSwitchToRequest, PermissionDecision, PermissionDecisionApproveOnce, @@ -67,6 +70,7 @@ CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, + McpOauthRequiredData, PermissionRequest, PermissionRequestedData, SessionCanvasClosedData, @@ -367,6 +371,63 @@ def approve_all( return PermissionDecisionApproveOnce() +# ============================================================================ +# MCP Auth Types +# ============================================================================ + + +class McpAuthWwwAuthenticateParams(TypedDict, total=False): + """Parsed parameters from an MCP server's WWW-Authenticate response.""" + + resourceMetadataUrl: str + scope: str + error: str + + +class McpAuthStaticClientConfig(TypedDict, total=False): + """Static OAuth client configuration supplied by the MCP server, if available.""" + + clientId: Required[str] + clientSecret: str + grantType: Literal["client_credentials"] + publicClient: bool + + +class McpAuthRequest(TypedDict, total=False): + """MCP OAuth request that the SDK host can satisfy with a host-acquired token.""" + + requestId: Required[str] + serverName: Required[str] + serverUrl: Required[str] + reason: Required[Literal["initial", "refresh", "reauth", "upscope"]] + wwwAuthenticateParams: McpAuthWwwAuthenticateParams + resourceMetadata: str + staticClientConfig: McpAuthStaticClientConfig + + +class McpAuthToken(TypedDict, total=False): + """Host-provided OAuth token data for a pending MCP OAuth request.""" + + accessToken: Required[str] + tokenType: str + expiresIn: int + + +class McpAuthResult(TypedDict, total=False): + """Result returned by an MCP auth request handler.""" + + kind: Required[Literal["token", "cancelled"]] + accessToken: str + tokenType: str + expiresIn: int + + +McpAuthHandler = Callable[ + [McpAuthRequest, dict[str, str]], + McpAuthResult | McpAuthToken | None | Awaitable[McpAuthResult | McpAuthToken | None], +] + + # ============================================================================ # User Input Request Types # ============================================================================ @@ -1340,6 +1401,8 @@ def __init__( self._tool_handlers_lock = threading.Lock() self._permission_handler: _PermissionHandlerFn | None = None self._permission_handler_lock = threading.Lock() + self._mcp_auth_handler: McpAuthHandler | None = None + self._mcp_auth_handler_lock = threading.Lock() self._user_input_handler: UserInputHandler | None = None self._user_input_handler_lock = threading.Lock() self._exit_plan_mode_handler: ExitPlanModeHandler | None = None @@ -1729,6 +1792,50 @@ def _handle_broadcast_event(self, event: SessionEvent) -> None: ) ) + case McpOauthRequiredData() as data: + with self._mcp_auth_handler_lock: + handler = self._mcp_auth_handler + if not data.request_id: + return + if not handler: + logger.warning( + "Received MCP OAuth request without a registered MCP auth handler. " + "SessionId=%s, RequestId=%s", + self.session_id, + data.request_id, + ) + return + request: McpAuthRequest = { + "requestId": data.request_id, + "serverName": data.server_name, + "serverUrl": data.server_url, + "reason": data.reason.value, + } + if data.www_authenticate_params is not None: + request["wwwAuthenticateParams"] = {} + if data.www_authenticate_params.resource_metadata_url is not None: + request["wwwAuthenticateParams"]["resourceMetadataUrl"] = ( + data.www_authenticate_params.resource_metadata_url + ) + if data.www_authenticate_params.scope is not None: + request["wwwAuthenticateParams"]["scope"] = data.www_authenticate_params.scope + if data.www_authenticate_params.error is not None: + request["wwwAuthenticateParams"]["error"] = data.www_authenticate_params.error + if data.resource_metadata is not None: + request["resourceMetadata"] = data.resource_metadata + if data.static_client_config is not None: + static_client_config: McpAuthStaticClientConfig = { + "clientId": data.static_client_config.client_id, + } + if data.static_client_config.client_secret is not None: + static_client_config["clientSecret"] = data.static_client_config.client_secret + if data.static_client_config.grant_type is not None: + static_client_config["grantType"] = data.static_client_config.grant_type + if data.static_client_config.public_client is not None: + static_client_config["publicClient"] = data.static_client_config.public_client + request["staticClientConfig"] = static_client_config + asyncio.ensure_future(self._execute_mcp_auth_and_respond(request, handler)) + case CommandExecuteData() as data: request_id = data.request_id command_name = data.command_name @@ -1942,6 +2049,57 @@ async def _execute_permission_and_respond( except (JsonRpcError, ProcessExitedError, OSError): pass # Connection lost or RPC error — nothing we can do + async def _execute_mcp_auth_and_respond( + self, + request: McpAuthRequest, + handler: McpAuthHandler, + ) -> None: + """Execute an MCP auth handler and respond via RPC.""" + request_id = request["requestId"] + try: + handler_start = time.perf_counter() + result = handler(request, {"session_id": self.session_id}) + if inspect.isawaitable(result): + result = await result + log_timing( + logger, + logging.DEBUG, + "CopilotSession._execute_mcp_auth_and_respond dispatch", + handler_start, + session_id=self.session_id, + request_id=request_id, + ) + + if result and result.get("kind", "token") == "token": + rpc_result = MCPOauthPendingRequestResponse( + kind=MCPOauthPendingRequestResponseKind.TOKEN, + access_token=result["accessToken"], + expires_in=result.get("expiresIn"), + token_type=result.get("tokenType"), + ) + else: + rpc_result = MCPOauthPendingRequestResponse( + kind=MCPOauthPendingRequestResponseKind.CANCELLED + ) + await self.rpc.mcp.oauth.handle_pending_request( + MCPOauthHandlePendingRequest( + request_id=request_id, + result=rpc_result, + ) + ) + except Exception: + try: + await self.rpc.mcp.oauth.handle_pending_request( + MCPOauthHandlePendingRequest( + request_id=request_id, + result=MCPOauthPendingRequestResponse( + kind=MCPOauthPendingRequestResponseKind.CANCELLED + ), + ) + ) + except (JsonRpcError, ProcessExitedError, OSError): + pass # Connection lost or RPC error — nothing we can do + async def _execute_command_and_respond( self, request_id: str, @@ -2126,6 +2284,11 @@ def _register_elicitation_handler(self, handler: ElicitationHandler | None) -> N with self._elicitation_handler_lock: self._elicitation_handler = handler + def _register_mcp_auth_handler(self, handler: McpAuthHandler | None) -> None: + """Register the MCP auth handler for this session.""" + with self._mcp_auth_handler_lock: + self._mcp_auth_handler = handler + def _register_exit_plan_mode_handler(self, handler: ExitPlanModeHandler | None) -> None: """Register the exit-plan-mode handler for this session.""" with self._exit_plan_mode_handler_lock: diff --git a/python/e2e/test_mcp_oauth_e2e.py b/python/e2e/test_mcp_oauth_e2e.py new file mode 100644 index 0000000000..520ee6cf11 --- /dev/null +++ b/python/e2e/test_mcp_oauth_e2e.py @@ -0,0 +1,266 @@ +import asyncio +import json +import os +from pathlib import Path +from typing import Any + +import httpx +import pytest + +from copilot.generated.rpc import MCPAppsCallToolRequest, MCPListToolsRequest +from copilot.session import MCPServerConfig, PermissionHandler +from copilot.session_events import McpServerStatus + +from .testharness import E2ETestContext, wait_for_condition + +TEST_MCP_OAUTH_SERVER = str( + (Path(__file__).parents[2] / "test" / "harness" / "test-mcp-oauth-server.mjs").resolve() +) +EXPECTED_TOKEN = "sdk-host-token" +REFRESH_TOKEN = f"{EXPECTED_TOKEN}-refresh" +UPSCOPE_TOKEN = f"{EXPECTED_TOKEN}-upscope" +REAUTH_TOKEN = f"{EXPECTED_TOKEN}-reauth" + +pytestmark = pytest.mark.asyncio(loop_scope="module") + + +async def _start_oauth_mcp_server() -> tuple[str, asyncio.subprocess.Process]: + process = await asyncio.create_subprocess_exec( + "node", + TEST_MCP_OAUTH_SERVER, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env={**os.environ, "EXPECTED_TOKEN": EXPECTED_TOKEN}, + ) + assert process.stdout is not None + + try: + line = await asyncio.wait_for(process.stdout.readline(), timeout=10) + except TimeoutError as exc: + await _stop_process(process) + assert process.stderr is not None + stderr = (await process.stderr.read()).decode(errors="replace") + raise TimeoutError(f"Timed out waiting for OAuth MCP server: {stderr}") from exc + if not line: + assert process.stderr is not None + stderr = (await process.stderr.read()).decode(errors="replace") + raise RuntimeError(f"OAuth MCP server exited before listening: {stderr}") + text = line.decode().strip() + if text.startswith("Listening: "): + return text.removeprefix("Listening: "), process + + await _stop_process(process) + raise RuntimeError(f"Unexpected OAuth MCP server startup line: {text}") + + +async def _stop_process(process: asyncio.subprocess.Process) -> None: + if process.returncode is not None: + return + process.terminate() + try: + await asyncio.wait_for(process.wait(), timeout=5) + except TimeoutError: + process.kill() + await process.wait() + + +async def _requests(base_url: str) -> list[dict[str, Any]]: + async with httpx.AsyncClient() as client: + response = await client.get(f"{base_url}/__requests") + response.raise_for_status() + return response.json() + + +async def _wait_for_mcp_server_status( + session, server_name: str, expected_status: McpServerStatus = McpServerStatus.CONNECTED +) -> None: + last_status = "" + + async def matches() -> bool: + nonlocal last_status + result = await session.rpc.mcp.list() + server = next((s for s in result.servers if s.name == server_name), None) + last_status = server.status.value if server is not None else "" + return server is not None and server.status == expected_status + + await wait_for_condition( + matches, + timeout=60.0, + poll_interval=0.2, + timeout_message=f"{server_name} did not reach {expected_status.value}; last status was {last_status}", + ) + + +class TestMcpOAuth: + async def test_should_satisfy_mcp_oauth_using_host_provided_token( + self, ctx: E2ETestContext + ): + url, process = await _start_oauth_mcp_server() + server_name = "oauth-protected-mcp" + observed_request = None + + def on_mcp_auth_request(request, _invocation): + nonlocal observed_request + observed_request = request + return { + "kind": "token", + "accessToken": EXPECTED_TOKEN, + "tokenType": "Bearer", + "expiresIn": 3600, + } + + try: + mcp_servers: dict[str, MCPServerConfig] = { + server_name: { + "type": "http", + "url": f"{url}/mcp", + "tools": ["*"], + } + } + async with await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + on_mcp_auth_request=on_mcp_auth_request, + mcp_servers=mcp_servers, + ) as session: + await _wait_for_mcp_server_status(session, server_name) + + tools = await session.rpc.mcp.list_tools( + MCPListToolsRequest(server_name=server_name) + ) + assert [tool.name for tool in tools.tools] == ["whoami"] + + assert observed_request is not None + assert observed_request["serverName"] == server_name + assert observed_request["serverUrl"] == f"{url}/mcp" + assert observed_request["reason"] == "initial" + assert observed_request["wwwAuthenticateParams"] == { + "resourceMetadataUrl": f"{url}/.well-known/oauth-protected-resource", + "scope": "mcp.read", + "error": "invalid_token", + } + assert json.loads(observed_request["resourceMetadata"]) == { + "resource": f"{url}/mcp", + "authorization_servers": [url], + "scopes_supported": ["mcp.read"], + "bearer_methods_supported": ["header"], + } + + requests = await _requests(url) + assert any(request["authorization"] is None for request in requests) + assert any( + request["authorization"] == f"Bearer {EXPECTED_TOKEN}" for request in requests + ) + finally: + await _stop_process(process) + + async def test_should_request_replacement_tokens_across_mcp_oauth_lifecycle( + self, ctx: E2ETestContext + ): + url, process = await _start_oauth_mcp_server() + server_name = "oauth-lifecycle-mcp" + observed_requests: list[dict[str, Any]] = [] + refresh_count = 0 + + def on_mcp_auth_request(request, _invocation): + nonlocal refresh_count + observed_requests.append(request) + if request["reason"] == "refresh": + refresh_count += 1 + assert request["wwwAuthenticateParams"] == {"error": "invalid_token"} + if refresh_count > 1: + return {"kind": "cancelled"} + return {"kind": "token", "accessToken": REFRESH_TOKEN} + if request["reason"] == "upscope": + assert request["wwwAuthenticateParams"] == { + "resourceMetadataUrl": f"{url}/.well-known/oauth-protected-resource", + "scope": "mcp.write", + "error": "insufficient_scope", + } + return {"kind": "token", "accessToken": UPSCOPE_TOKEN} + if request["reason"] == "reauth": + return {"kind": "token", "accessToken": REAUTH_TOKEN} + return {"kind": "token", "accessToken": EXPECTED_TOKEN} + + try: + mcp_servers: dict[str, MCPServerConfig] = { + server_name: { + "type": "http", + "url": f"{url}/mcp", + "tools": ["*"], + } + } + async with await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + on_mcp_auth_request=on_mcp_auth_request, + mcp_servers=mcp_servers, + enable_mcp_apps=True, + ) as session: + await _wait_for_mcp_server_status(session, server_name) + + for scenario in ("refresh", "upscope", "reauth"): + result = await session.rpc.mcp.apps.call_tool( + MCPAppsCallToolRequest( + origin_server_name=server_name, + server_name=server_name, + tool_name="whoami", + arguments={"scenario": scenario}, + ) + ) + assert result["content"] == [ + {"type": "text", "text": "oauth-test-user"} + ] + + assert [request["reason"] for request in observed_requests] == [ + "initial", + "refresh", + "upscope", + "refresh", + "reauth", + ] + requests = await _requests(url) + assert any( + request["authorization"] == f"Bearer {REFRESH_TOKEN}" for request in requests + ) + assert any( + request["authorization"] == f"Bearer {UPSCOPE_TOKEN}" for request in requests + ) + assert any( + request["authorization"] == f"Bearer {REAUTH_TOKEN}" for request in requests + ) + finally: + await _stop_process(process) + + async def test_should_cancel_pending_mcp_oauth_request( + self, ctx: E2ETestContext + ): + url, process = await _start_oauth_mcp_server() + server_name = "oauth-cancelled-mcp" + observed_request = None + + def on_mcp_auth_request(request, _invocation): + nonlocal observed_request + observed_request = request + return {"kind": "cancelled"} + + try: + mcp_servers: dict[str, MCPServerConfig] = { + server_name: { + "type": "http", + "url": f"{url}/mcp", + "tools": ["*"], + } + } + async with await ctx.client.create_session( + on_permission_request=PermissionHandler.approve_all, + on_mcp_auth_request=on_mcp_auth_request, + mcp_servers=mcp_servers, + ) as session: + await _wait_for_mcp_server_status( + session, server_name, McpServerStatus.FAILED + ) + + assert observed_request is not None + assert observed_request["serverName"] == server_name + assert observed_request["reason"] == "initial" + finally: + await _stop_process(process) diff --git a/python/e2e/testharness/context.py b/python/e2e/testharness/context.py index 735c365c5f..f83544e8e8 100644 --- a/python/e2e/testharness/context.py +++ b/python/e2e/testharness/context.py @@ -18,6 +18,10 @@ from .proxy import CapiProxy +LOCAL_RUNTIME_CLI_PATH = Path( + "/Users/roji/.copilot/repos/copilot-worktrees/copilot-agent-runtime/roji-symmetrical-dollop/dist-cli/index.js" +) + def get_cli_path_for_tests() -> str: """Get CLI path for E2E tests. @@ -28,6 +32,9 @@ def get_cli_path_for_tests() -> str: if env_path and Path(env_path).exists(): return str(Path(env_path).resolve()) + if LOCAL_RUNTIME_CLI_PATH.exists(): + return str(LOCAL_RUNTIME_CLI_PATH) + # Look for CLI in sibling nodejs directory's node_modules. As of CLI 1.0.64-1 # the @github/copilot package is a thin loader; the runnable index.js ships in # the installed platform package (e.g. @github/copilot-linux-x64). @@ -168,6 +175,8 @@ def get_env(self) -> dict: "XDG_CONFIG_HOME": self.home_dir, "XDG_STATE_HOME": self.home_dir, "GITHUB_TOKEN": DEFAULT_GITHUB_TOKEN, + "COPILOT_MCP_APPS": "true", + "MCP_APPS": "true", } ) return env diff --git a/python/test_client.py b/python/test_client.py index f3f46c4d8b..0b41a34de2 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -4,6 +4,7 @@ This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.py instead. """ +import asyncio from datetime import UTC, datetime from unittest.mock import AsyncMock, Mock, patch @@ -28,6 +29,14 @@ ModelSupports, ) from copilot.session import PermissionHandler +from copilot.session_events import ( + McpOauthRequestReason, + McpOauthRequiredData, + McpOauthRequiredStaticClientConfig, + McpOauthWWWAuthenticateParams, + SessionEvent, + SessionEventType, +) from e2e.testharness import CLI_PATH @@ -139,6 +148,306 @@ async def test_resume_session_allows_none_permission_handler(self): class TestCreateSessionConfig: + @pytest.mark.asyncio + async def test_mcp_auth_handler_registers_interest_in_create_session(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured: list[tuple[str, dict]] = [] + + async def mock_request(method, params, **kwargs): + captured.append((method, params)) + if method == "session.eventLog.registerInterest": + return {"id": "interest-1"} + if method == "session.create": + result = {"sessionId": params["sessionId"], "workspacePath": None} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + await client.create_session( + on_permission_request=PermissionHandler.approve_all, + on_mcp_auth_request=lambda request: {"kind": "cancelled"}, + ) + + create_method, create_payload = captured[0] + interest_method, interest_payload = captured[1] + assert create_method == "session.create" + assert interest_method == "session.eventLog.registerInterest" + assert interest_payload["eventType"] == "mcp.oauth_required" + assert interest_payload["sessionId"] == create_payload["sessionId"] + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_mcp_auth_interest_is_not_registered_without_handler(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured: list[tuple[str, dict]] = [] + + async def mock_request(method, params, **kwargs): + captured.append((method, params)) + if method == "session.create": + result = {"sessionId": params["sessionId"], "workspacePath": None} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + if method == "session.resume": + return {"sessionId": params["sessionId"], "workspacePath": None} + return {} + + client._client.request = mock_request + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + on_event=lambda event: None, + ) + await client.resume_session( + "session-without-auth", + on_permission_request=PermissionHandler.approve_all, + on_event=lambda event: None, + ) + + assert session.session_id + assert not any( + method == "session.eventLog.registerInterest" + and params["eventType"] == "mcp.oauth_required" + for method, params in captured + ) + assert any( + method == "session.create" and params["requestPermission"] is True + for method, params in captured + ) + assert any( + method == "session.resume" and params["requestPermission"] is True + for method, params in captured + ) + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_mcp_auth_handler_registers_interest_before_resume(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured: list[tuple[str, dict]] = [] + + async def mock_request(method, params, **kwargs): + captured.append((method, params)) + if method == "session.eventLog.registerInterest": + return {"id": "interest-1"} + if method == "session.resume": + return {"sessionId": params["sessionId"], "workspacePath": None} + return {} + + client._client.request = mock_request + await client.resume_session( + "session-with-auth", + on_permission_request=PermissionHandler.approve_all, + on_mcp_auth_request=lambda request: {"kind": "cancelled"}, + ) + + interest_method, interest_payload = captured[0] + resume_method, resume_payload = captured[1] + assert interest_method == "session.eventLog.registerInterest" + assert interest_payload == { + "sessionId": "session-with-auth", + "eventType": "mcp.oauth_required", + } + assert resume_method == "session.resume" + assert resume_payload["requestPermission"] is True + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_mcp_auth_handler_registers_interest_after_cloud_create_only_with_handler(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured: list[tuple[str, dict]] = [] + create_count = 0 + + async def mock_request(method, params, **kwargs): + nonlocal create_count + captured.append((method, params)) + if method == "session.eventLog.registerInterest": + return {"id": "interest-1"} + if method == "session.create": + create_count += 1 + result = { + "sessionId": f"server-assigned-session-{create_count}", + "workspacePath": None, + } + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + cloud = CloudSessionOptions( + repository=CloudSessionRepository( + owner="github", + name="copilot-sdk", + branch="main", + ) + ) + + client._client.request = mock_request + await client.create_session( + on_permission_request=PermissionHandler.approve_all, + cloud=cloud, + ) + + assert not any( + method == "session.eventLog.registerInterest" + and params["eventType"] == "mcp.oauth_required" + for method, params in captured + ) + + captured.clear() + await client.create_session( + on_permission_request=PermissionHandler.approve_all, + on_mcp_auth_request=lambda request: {"kind": "cancelled"}, + cloud=cloud, + ) + + create_method, _create_payload = captured[0] + interest_method, interest_payload = captured[1] + assert create_method == "session.create" + assert interest_method == "session.eventLog.registerInterest" + assert interest_payload == { + "sessionId": "server-assigned-session-2", + "eventType": "mcp.oauth_required", + } + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_mcp_auth_required_event_sends_host_token(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured: list[tuple[str, dict]] = [] + + async def mock_request(method, params, **kwargs): + if method == "session.mcp.oauth.handlePendingRequest": + captured.append((method, params)) + return {"success": True} + if method == "session.create": + result = {"sessionId": params["sessionId"], "workspacePath": None} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + if method == "session.eventLog.registerInterest": + return {"id": "interest-1"} + return {} + + client._client.request = mock_request + observed_request = None + + def handle_mcp_auth_request(request, invocation): + nonlocal observed_request + observed_request = request + return { + "accessToken": "host-token", + "tokenType": "Bearer", + } + + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + on_mcp_auth_request=handle_mcp_auth_request, + ) + + session._dispatch_event( + SessionEvent( + data=McpOauthRequiredData( + request_id="oauth-request", + server_name="oauth-server", + server_url="https://example.com/mcp", + reason=McpOauthRequestReason.INITIAL, + www_authenticate_params=McpOauthWWWAuthenticateParams( + resource_metadata_url="https://example.com/.well-known/oauth-protected-resource" + ), + resource_metadata='{"resource":"https://example.com/mcp"}', + static_client_config=McpOauthRequiredStaticClientConfig( + client_id="static-client", + client_secret="static-secret", + grant_type="client_credentials", + public_client=False, + ), + ), + id="evt-1", + timestamp="2026-01-01T00:00:00Z", + type=SessionEventType.MCP_OAUTH_REQUIRED, + ephemeral=True, + parent_id=None, + ) + ) + + for _ in range(200): + if captured: + break + await asyncio.sleep(0.005) + + assert observed_request is not None + assert observed_request["resourceMetadata"] == '{"resource":"https://example.com/mcp"}' + assert observed_request["wwwAuthenticateParams"]["resourceMetadataUrl"] == ( + "https://example.com/.well-known/oauth-protected-resource" + ) + assert observed_request["staticClientConfig"] == { + "clientId": "static-client", + "clientSecret": "static-secret", + "grantType": "client_credentials", + "publicClient": False, + } + assert captured == [ + ( + "session.mcp.oauth.handlePendingRequest", + { + "sessionId": session.session_id, + "requestId": "oauth-request", + "result": { + "kind": "token", + "accessToken": "host-token", + "tokenType": "Bearer", + }, + }, + ) + ] + + observed_request = None + session._dispatch_event( + SessionEvent( + data=McpOauthRequiredData( + request_id="oauth-request-without-metadata", + server_name="oauth-server", + server_url="https://example.com/mcp", + reason=McpOauthRequestReason.INITIAL, + ), + id="evt-2", + timestamp="2026-01-01T00:00:00Z", + type=SessionEventType.MCP_OAUTH_REQUIRED, + ephemeral=True, + parent_id=None, + ) + ) + + for _ in range(200): + if observed_request is not None: + break + await asyncio.sleep(0.005) + + assert observed_request is not None + assert "resourceMetadata" not in observed_request + assert "wwwAuthenticateParams" not in observed_request + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_create_session_forwards_cloud_options(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/rust/src/generated/api_types.rs b/rust/src/generated/api_types.rs index b1d85c0a58..36dc2478cc 100644 --- a/rust/src/generated/api_types.rs +++ b/rust/src/generated/api_types.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use super::session_events::{ AbortReason, ContextTier, McpServerSource, McpServerStatus, PermissionPromptRequest, - PermissionRule, ReasoningSummary, SessionMode, ShutdownType, SkillSource, + PermissionRule, ReasoningSummary, ResponseBudgetConfig, SessionMode, ShutdownType, SkillSource, UserToolSessionApproval, }; use crate::types::{RequestId, SessionEvent, SessionId}; @@ -321,6 +321,9 @@ pub mod rpc_methods { "session.mcp.oauth.handlePendingRequest"; /// `session.mcp.oauth.login` pub const SESSION_MCP_OAUTH_LOGIN: &str = "session.mcp.oauth.login"; + /// `session.mcp.headers.handlePendingHeadersRefreshRequest` + pub const SESSION_MCP_HEADERS_HANDLEPENDINGHEADERSREFRESHREQUEST: &str = + "session.mcp.headers.handlePendingHeadersRefreshRequest"; /// `session.mcp.apps.readResource` pub const SESSION_MCP_APPS_READRESOURCE: &str = "session.mcp.apps.readResource"; /// `session.mcp.apps.listTools` @@ -4285,6 +4288,52 @@ pub struct McpFilteredServer { pub redacted_reason: Option, } +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpHeadersHandlePendingHeadersRefreshRequestHeaders { + /// Headers to overlay onto the MCP request. Dynamic headers override static config headers but do not replace SDK-managed request headers. + pub headers: HashMap, + pub kind: McpHeadersHandlePendingHeadersRefreshRequestHeadersKind, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpHeadersHandlePendingHeadersRefreshRequestNone { + pub kind: McpHeadersHandlePendingHeadersRefreshRequestNoneKind, +} + +/// MCP headers refresh request id and the host response. +/// +///

+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpHeadersHandlePendingHeadersRefreshRequestRequest { + /// Headers refresh request identifier from mcp.headers_refresh_required + pub request_id: RequestId, + /// Host response: supply dynamic headers or decline this refresh. + pub result: McpHeadersHandlePendingHeadersRefreshRequest, +} + +/// Indicates whether the pending MCP headers refresh response was accepted. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpHeadersHandlePendingHeadersRefreshRequestResult { + /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. + pub success: bool, +} + /// Recorded MCP server connection failure. /// ///
@@ -4431,9 +4480,6 @@ pub struct McpOauthPendingRequestResponseToken { #[serde(skip_serializing_if = "Option::is_none")] pub expires_in: Option, pub kind: McpOauthPendingRequestResponseTokenKind, - /// Refresh token supplied by the host, if available. - #[serde(skip_serializing_if = "Option::is_none")] - pub refresh_token: Option, /// OAuth token type. Defaults to Bearer when omitted. #[serde(skip_serializing_if = "Option::is_none")] pub token_type: Option, @@ -5221,6 +5267,9 @@ pub struct ModelBillingTokenPrices { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ModelBilling { + /// Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. + #[serde(skip_serializing_if = "Option::is_none")] + pub discount_percent: Option, /// Billing cost multiplier relative to the base rate #[serde(skip_serializing_if = "Option::is_none")] pub multiplier: Option, @@ -9793,6 +9842,9 @@ pub struct SessionOpenOptions { /// Runtime context discriminator for agent filtering. #[serde(skip_serializing_if = "Option::is_none")] pub agent_context: Option, + /// Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_all_mcp_server_instructions: Option, /// Whether ask_user is explicitly disabled. #[serde(skip_serializing_if = "Option::is_none")] pub ask_user_disabled: Option, @@ -9941,6 +9993,9 @@ pub struct SessionOpenOptions { /// Whether this session supports remote steering. #[serde(skip_serializing_if = "Option::is_none")] pub remote_steerable: Option, + /// Initial experimental response budget limits for the session. + #[serde(skip_serializing_if = "Option::is_none")] + pub response_budget: Option, /// Whether the host is an interactive UI. #[serde(skip_serializing_if = "Option::is_none")] pub running_in_interactive_mode: Option, @@ -10884,6 +10939,9 @@ pub struct SessionUpdateOptionsParams { /// Runtime context discriminator (e.g., `cli`, `actions`). #[serde(skip_serializing_if = "Option::is_none")] pub agent_context: Option, + /// Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. + #[serde(skip_serializing_if = "Option::is_none")] + pub allow_all_mcp_server_instructions: Option, /// Whether to disable the `ask_user` tool (encourages autonomous behavior). #[serde(skip_serializing_if = "Option::is_none")] pub ask_user_disabled: Option, @@ -10992,6 +11050,9 @@ pub struct SessionUpdateOptionsParams { /// Reasoning summary mode for supported model clients. #[serde(skip_serializing_if = "Option::is_none")] pub reasoning_summary: Option, + /// Optional experimental response budget limits. Pass null to clear the response budget. + #[serde(skip_serializing_if = "Option::is_none")] + pub response_budget: Option, /// Whether the session is running in an interactive UI. #[serde(skip_serializing_if = "Option::is_none")] pub running_in_interactive_mode: Option, @@ -11532,6 +11593,12 @@ pub struct SubagentSettings { /// Names of subagents the user has turned off; they cannot be dispatched #[serde(skip_serializing_if = "Option::is_none")] pub disabled_subagents: Option>, + /// Maximum number of subagents that can run concurrently; applies to usage-based billing users only + #[serde(skip_serializing_if = "Option::is_none")] + pub max_concurrency: Option, + /// Maximum subagent nesting depth; applies to usage-based billing users only + #[serde(skip_serializing_if = "Option::is_none")] + pub max_depth: Option, } /// Schema for the `TaskAgentInfo` type. @@ -12658,6 +12725,12 @@ pub struct UpdateSubagentSettingsRequestSubagents { /// Names of subagents the user has turned off; they cannot be dispatched #[serde(skip_serializing_if = "Option::is_none")] pub disabled_subagents: Option>, + /// Maximum number of subagents that can run concurrently; applies to usage-based billing users only + #[serde(skip_serializing_if = "Option::is_none")] + pub max_concurrency: Option, + /// Maximum subagent nesting depth; applies to usage-based billing users only + #[serde(skip_serializing_if = "Option::is_none")] + pub max_depth: Option, } /// Subagent settings to apply to the current session @@ -15178,6 +15251,21 @@ pub struct SessionMcpOauthLoginResult { pub authorization_url: Option, } +/// Indicates whether the pending MCP headers refresh response was accepted. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionMcpHeadersHandlePendingHeadersRefreshRequestResult { + /// Whether the response was accepted. False if the request was unknown, timed out, or already resolved. + pub success: bool, +} + /// Resource contents returned by the MCP server. /// ///
@@ -18123,6 +18211,35 @@ pub enum McpAppsSetHostContextDetailsTheme { Unknown, } +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpHeadersHandlePendingHeadersRefreshRequestHeadersKind { + #[serde(rename = "headers")] + #[default] + Headers, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpHeadersHandlePendingHeadersRefreshRequestNoneKind { + #[serde(rename = "none")] + #[default] + None, +} + +/// Host response: supply dynamic headers or decline this refresh. +/// +///
+/// +/// **Experimental.** This type is part of an experimental wire-protocol surface +/// and may change or be removed in future SDK or CLI releases. +/// +///
+#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum McpHeadersHandlePendingHeadersRefreshRequest { + Headers(McpHeadersHandlePendingHeadersRefreshRequestHeaders), + None(McpHeadersHandlePendingHeadersRefreshRequestNone), +} + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum McpOauthPendingRequestResponseTokenKind { #[serde(rename = "token")] @@ -19945,7 +20062,7 @@ pub enum SlashCommandSelectSubcommandResultKind { SelectSubcommand, } -/// Result of invoking the slash command (text output, prompt to send to the agent, or completion). +/// Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). /// ///
/// diff --git a/rust/src/generated/rpc.rs b/rust/src/generated/rpc.rs index 57a5192dca..aad601456e 100644 --- a/rust/src/generated/rpc.rs +++ b/rust/src/generated/rpc.rs @@ -3143,7 +3143,7 @@ impl<'a> SessionRpcCommands<'a> { /// /// # Returns /// - /// Result of invoking the slash command (text output, prompt to send to the agent, or completion). + /// Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). /// ///
/// @@ -3887,6 +3887,13 @@ impl<'a> SessionRpcMcp<'a> { } } + /// `session.mcp.headers.*` sub-namespace. + pub fn headers(&self) -> SessionRpcMcpHeaders<'a> { + SessionRpcMcpHeaders { + session: self.session, + } + } + /// `session.mcp.oauth.*` sub-namespace. pub fn oauth(&self) -> SessionRpcMcpOauth<'a> { SessionRpcMcpOauth { @@ -4600,6 +4607,50 @@ impl<'a> SessionRpcMcpApps<'a> { } } +/// `session.mcp.headers.*` RPCs. +#[derive(Clone, Copy)] +pub struct SessionRpcMcpHeaders<'a> { + pub(crate) session: &'a Session, +} + +impl<'a> SessionRpcMcpHeaders<'a> { + /// Responds to a pending MCP dynamic headers refresh request. Hosts that subscribe to `mcp.headers_refresh_required` use this to provide short-lived per-server headers or to indicate that no dynamic headers are available for this refresh. + /// + /// Wire method: `session.mcp.headers.handlePendingHeadersRefreshRequest`. + /// + /// # Parameters + /// + /// * `params` - MCP headers refresh request id and the host response. + /// + /// # Returns + /// + /// Indicates whether the pending MCP headers refresh response was accepted. + /// + ///
+ /// + /// **Experimental.** This API is part of an experimental wire-protocol surface + /// and may change or be removed in future SDK or CLI releases. Pin both the + /// SDK and CLI versions if your code depends on it. + /// + ///
+ pub async fn handle_pending_headers_refresh_request( + &self, + params: McpHeadersHandlePendingHeadersRefreshRequestRequest, + ) -> Result { + let mut wire_params = serde_json::to_value(params)?; + wire_params["sessionId"] = serde_json::Value::String(self.session.id().to_string()); + let _value = self + .session + .client() + .call( + rpc_methods::SESSION_MCP_HEADERS_HANDLEPENDINGHEADERSREFRESHREQUEST, + Some(wire_params), + ) + .await?; + Ok(serde_json::from_value(_value)?) + } +} + /// `session.mcp.oauth.*` RPCs. #[derive(Clone, Copy)] pub struct SessionRpcMcpOauth<'a> { diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index e094ec3655..36fecc054b 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -85,6 +85,8 @@ pub enum SessionEventType { AssistantMessageDelta, #[serde(rename = "assistant.turn_end")] AssistantTurnEnd, + #[serde(rename = "assistant.idle")] + AssistantIdle, #[serde(rename = "assistant.usage")] AssistantUsage, #[serde(rename = "model.call_failure")] @@ -152,6 +154,10 @@ pub enum SessionEventType { McpOauthRequired, #[serde(rename = "mcp.oauth_completed")] McpOauthCompleted, + #[serde(rename = "mcp.headers_refresh_required")] + McpHeadersRefreshRequired, + #[serde(rename = "mcp.headers_refresh_completed")] + McpHeadersRefreshCompleted, #[serde(rename = "session.custom_notification")] SessionCustomNotification, #[serde(rename = "external_tool.requested")] @@ -336,6 +342,8 @@ pub enum SessionEventData { AssistantMessageDelta(AssistantMessageDeltaData), #[serde(rename = "assistant.turn_end")] AssistantTurnEnd(AssistantTurnEndData), + #[serde(rename = "assistant.idle")] + AssistantIdle(AssistantIdleData), #[serde(rename = "assistant.usage")] AssistantUsage(AssistantUsageData), #[serde(rename = "model.call_failure")] @@ -396,6 +404,10 @@ pub enum SessionEventData { McpOauthRequired(McpOauthRequiredData), #[serde(rename = "mcp.oauth_completed")] McpOauthCompleted(McpOauthCompletedData), + #[serde(rename = "mcp.headers_refresh_required")] + McpHeadersRefreshRequired(McpHeadersRefreshRequiredData), + #[serde(rename = "mcp.headers_refresh_completed")] + McpHeadersRefreshCompleted(McpHeadersRefreshCompletedData), #[serde(rename = "session.custom_notification")] SessionCustomNotification(SessionCustomNotificationData), #[serde(rename = "external_tool.requested")] @@ -550,6 +562,18 @@ pub struct WorkingDirectoryContext { pub repository_host: Option, } +/// Optional response budget limits. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ResponseBudgetConfig { + /// Maximum AI Credits allowed while responding to one top-level user message. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_ai_credits: Option, + /// Maximum model-call iterations allowed while responding to one top-level user message. + #[serde(skip_serializing_if = "Option::is_none")] + pub max_model_iterations: Option, +} + /// Session event "session.start". Session initialization metadata including context and configuration #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -579,6 +603,9 @@ pub struct SessionStartData { /// Whether this session supports remote steering via GitHub #[serde(skip_serializing_if = "Option::is_none")] pub remote_steerable: Option, + /// Response budget limits configured at session creation time, if any + #[serde(skip_serializing_if = "Option::is_none")] + pub response_budget: Option, /// Model selected at session creation time, if any #[serde(skip_serializing_if = "Option::is_none")] pub selected_model: Option, @@ -620,6 +647,9 @@ pub struct SessionResumeData { /// Whether this session supports remote steering via GitHub #[serde(skip_serializing_if = "Option::is_none")] pub remote_steerable: Option, + /// Response budget limits currently configured at resume time; null when no budget is active + #[serde(skip_serializing_if = "Option::is_none")] + pub response_budget: Option, /// ISO 8601 timestamp when the session was resumed pub resume_time: String, /// Model currently selected at resume time @@ -1274,6 +1304,9 @@ pub struct UserMessageData { pub attachments: Option>, /// The user's message text as displayed in the timeline pub content: String, + /// How this message was delivered to the agentic loop relative to loop state (idle-start vs. steering/queued while busy). The timing axis; combine with `source` (origin) for the full picture. Used for telemetry attribution. + #[serde(skip_serializing_if = "Option::is_none")] + pub delivery: Option, /// CAPI interaction ID for correlating this user message with its turn #[serde(skip_serializing_if = "Option::is_none")] pub interaction_id: Option, @@ -1583,6 +1616,15 @@ pub struct AssistantTurnEndData { pub turn_id: String, } +/// Session event "assistant.idle". Payload emitted whenever the main agent's processing loop goes idle, including while related background work (running agents or in-flight attached shell commands) is still pending and the session-level idle event is therefore deferred +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AssistantIdleData { + /// True when the preceding agentic loop was cancelled via abort signal + #[serde(skip_serializing_if = "Option::is_none")] + pub aborted: Option, +} + /// Token usage detail for a single billing category #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1820,6 +1862,16 @@ pub struct ToolUserRequestedData { pub tool_name: String, } +/// Shell-aware path hints for a shell tool's command, captured at start time so consumers can snapshot a file's pre-image before the tool runs. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolExecutionStartShellToolInfo { + /// Whether the command includes a file write redirection (e.g., > or >>). + pub has_write_file_redirection: bool, + /// File paths the command may read or write, derived from the command at start time. Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. + pub possible_paths: Vec, +} + /// Schema for the `ToolExecutionStartToolDescriptionMetaUI` type. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -1879,6 +1931,9 @@ pub struct ToolExecutionStartData { #[deprecated] #[serde(skip_serializing_if = "Option::is_none")] pub parent_tool_call_id: Option, + /// Shell-tool path hints derived from the command at start time for shell tools (bash/powershell/local_shell). Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. Absent for non-shell tools. + #[serde(skip_serializing_if = "Option::is_none")] + pub shell_tool_info: Option, /// Unique identifier for this tool call pub tool_call_id: String, /// Tool definition metadata, present for MCP tools with MCP Apps support @@ -3274,6 +3329,9 @@ pub struct SamplingCompletedData { pub struct McpOauthRequiredStaticClientConfig { /// OAuth client ID for the server pub client_id: String, + /// Optional OAuth client secret for confidential static clients, when the runtime can resolve one + #[serde(skip_serializing_if = "Option::is_none")] + pub client_secret: Option, /// Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). #[serde(skip_serializing_if = "Option::is_none")] pub grant_type: Option, @@ -3289,8 +3347,9 @@ pub struct McpOauthWWWAuthenticateParams { /// OAuth error from the WWW-Authenticate error parameter, if present #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, - /// Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter - pub resource_metadata_url: String, + /// Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter, if present + #[serde(skip_serializing_if = "Option::is_none")] + pub resource_metadata_url: Option, /// Requested OAuth scopes from the WWW-Authenticate scope parameter, if present #[serde(skip_serializing_if = "Option::is_none")] pub scope: Option, @@ -3300,6 +3359,8 @@ pub struct McpOauthWWWAuthenticateParams { #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct McpOauthRequiredData { + /// Why the runtime is requesting host-provided OAuth credentials. + pub reason: McpOauthRequestReason, /// Unique identifier for this OAuth request; used to respond via session.mcp.oauth.handlePendingRequest pub request_id: RequestId, /// Raw OAuth protected-resource metadata document fetched for the MCP server, if available @@ -3327,6 +3388,30 @@ pub struct McpOauthCompletedData { pub request_id: RequestId, } +/// Session event "mcp.headers_refresh_required". Dynamic headers refresh request for a remote MCP server +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpHeadersRefreshRequiredData { + /// Why dynamic headers are being requested. + pub reason: McpHeadersRefreshRequiredReason, + /// Unique identifier for this headers refresh request; used to respond via session.mcp.headers.handlePendingHeadersRefreshRequest() + pub request_id: RequestId, + /// Display name of the remote MCP server requesting headers + pub server_name: String, + /// URL of the remote MCP server requesting headers + pub server_url: String, +} + +/// Session event "mcp.headers_refresh_completed". MCP headers refresh request completion notification +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct McpHeadersRefreshCompletedData { + /// How the pending MCP headers refresh request resolved. + pub outcome: McpHeadersRefreshCompletedOutcome, + /// Request ID of the resolved headers refresh request + pub request_id: RequestId, +} + /// Session event "session.custom_notification". Opaque custom notification data. Consumers may branch on source and name, but payload semantics are source-defined. #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -4094,6 +4179,24 @@ pub enum UserMessageAgentMode { Unknown, } +/// How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum UserMessageDelivery { + /// Delivered while the loop was idle; starts its own run immediately (a human's fresh turn, or a system notification waking an idle agent). + #[serde(rename = "idle")] + Idle, + /// Injected into the current in-flight run while the agent was busy (immediate mode). + #[serde(rename = "steering")] + Steering, + /// Enqueued while the agent was busy; processed as its own run afterward. + #[serde(rename = "queued")] + Queued, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// The system that produced a citation. /// ///
@@ -4824,6 +4927,27 @@ pub enum ElicitationCompletedAction { Unknown, } +/// Reason the runtime is requesting host-provided MCP OAuth credentials +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpOauthRequestReason { + /// Initial credentials are required before connecting to the MCP server. + #[serde(rename = "initial")] + Initial, + /// The current host-provided credential was rejected and a replacement is requested. + #[serde(rename = "refresh")] + Refresh, + /// The server requires a new host authorization flow before continuing. + #[serde(rename = "reauth")] + Reauth, + /// The server requires a credential with additional scope or audience. + #[serde(rename = "upscope")] + Upscope, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum McpOauthRequiredStaticClientConfigGrantType { @@ -4847,6 +4971,42 @@ pub enum McpOauthCompletionOutcome { Unknown, } +/// Why dynamic headers are being requested. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpHeadersRefreshRequiredReason { + /// The transport is making its first dynamic header request for this server. + #[serde(rename = "startup")] + Startup, + /// The previously cached dynamic headers expired. + #[serde(rename = "ttl-expired")] + TtlExpired, + /// The server returned 401 and stale dynamic headers were invalidated. + #[serde(rename = "auth-failed")] + AuthFailed, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + +/// How the pending MCP headers refresh request resolved. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum McpHeadersRefreshCompletedOutcome { + /// The host supplied dynamic headers. + #[serde(rename = "headers")] + Headers, + /// The host responded with no dynamic headers. + #[serde(rename = "none")] + None, + /// No response arrived within the bounded window. + #[serde(rename = "timeout")] + Timeout, + /// Unknown variant for forward compatibility. + #[default] + #[serde(other)] + Unknown, +} + /// The user's auto-mode-switch choice #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum AutoModeSwitchResponse { diff --git a/rust/src/handler.rs b/rust/src/handler.rs index dadd1706ff..22a16727ed 100644 --- a/rust/src/handler.rs +++ b/rust/src/handler.rs @@ -19,8 +19,13 @@ use async_trait::async_trait; use serde::{Deserialize, Serialize}; use crate::generated::api_types::{ - PermissionDecision, PermissionDecisionApproveOnce, PermissionDecisionReject, - PermissionDecisionUserNotAvailable, + McpOauthPendingRequestResponse, McpOauthPendingRequestResponseCancelled, + McpOauthPendingRequestResponseCancelledKind, McpOauthPendingRequestResponseToken, + McpOauthPendingRequestResponseTokenKind, PermissionDecision, PermissionDecisionApproveOnce, + PermissionDecisionReject, PermissionDecisionUserNotAvailable, +}; +use crate::session_events::{ + McpOauthRequestReason, McpOauthRequiredStaticClientConfig, McpOauthWWWAuthenticateParams, }; use crate::types::{ ElicitationRequest, ElicitationResult, ExitPlanModeData, PermissionRequestData, RequestId, @@ -159,6 +164,73 @@ pub trait ElicitationHandler: Send + Sync + 'static { ) -> ElicitationResult; } +/// MCP OAuth request that the SDK host can satisfy with a host-acquired token. +#[derive(Debug, Clone)] +pub struct McpAuthRequest { + /// Display name of the MCP server that requires OAuth. + pub server_name: String, + /// URL of the MCP server that requires OAuth. + pub server_url: String, + /// Why the runtime is requesting host-provided OAuth credentials. + pub reason: McpOauthRequestReason, + /// Parsed WWW-Authenticate parameters from the MCP server, if available. + pub www_authenticate_params: Option, + /// Raw RFC 9728 protected-resource metadata JSON fetched by the runtime, if available. + pub resource_metadata: Option, + /// Static OAuth client configuration, if the server specifies one. + pub static_client_config: Option, +} + +/// Result returned by an MCP auth request handler. +#[derive(Debug, Clone)] +pub enum McpAuthResult { + /// Supplies host-acquired OAuth token data. + Token { + /// Access token acquired by the SDK host. + access_token: String, + /// OAuth token type. Defaults to Bearer when omitted. + token_type: Option, + /// Token lifetime in seconds, if known. + expires_in: Option, + }, + /// Declines or cancels the pending OAuth request. + Cancelled, +} + +impl McpAuthResult { + pub(crate) fn into_wire(self) -> McpOauthPendingRequestResponse { + match self { + Self::Token { + access_token, + token_type, + expires_in, + } => McpOauthPendingRequestResponse::Token(McpOauthPendingRequestResponseToken { + access_token, + token_type, + expires_in, + kind: McpOauthPendingRequestResponseTokenKind::Token, + }), + Self::Cancelled => { + McpOauthPendingRequestResponse::Cancelled(McpOauthPendingRequestResponseCancelled { + kind: McpOauthPendingRequestResponseCancelledKind::Cancelled, + }) + } + } + } +} + +/// Handler for MCP server OAuth requests. +#[async_trait] +pub trait McpAuthHandler: Send + Sync + 'static { + /// Resolve an MCP OAuth request with host token data or cancellation. + async fn handle( + &self, + session_id: SessionId, + request_id: RequestId, + request: McpAuthRequest, + ) -> McpAuthResult; +} + /// Handler for `user_input.requested` events from the `ask_user` tool. /// /// When unset, `requestUserInput: false` goes on the wire and the @@ -266,4 +338,23 @@ mod tests { PermissionResult::Decision(PermissionDecision::Reject(_)) )); } + + #[test] + fn mcp_auth_result_token_converts_to_wire_response() { + let wire = McpAuthResult::Token { + access_token: "host-token".to_string(), + token_type: Some("Bearer".to_string()), + expires_in: Some(3600), + } + .into_wire(); + + match wire { + McpOauthPendingRequestResponse::Token(token) => { + assert_eq!(token.access_token, "host-token"); + assert_eq!(token.token_type.as_deref(), Some("Bearer")); + assert_eq!(token.expires_in, Some(3600)); + } + McpOauthPendingRequestResponse::Cancelled(_) => panic!("expected token response"), + } + } } diff --git a/rust/src/session.rs b/rust/src/session.rs index 18b91b4377..971e7b9e4b 100644 --- a/rust/src/session.rs +++ b/rust/src/session.rs @@ -11,14 +11,17 @@ use tokio_util::sync::CancellationToken; use tracing::{Instrument, warn}; use crate::canvas::CanvasHandler; -use crate::generated::api_types::{LogRequest, ModelSwitchToRequest, OpenCanvasInstance}; +use crate::generated::api_types::{ + LogRequest, ModelSwitchToRequest, OpenCanvasInstance, RegisterEventInterestParams, rpc_methods, +}; use crate::generated::session_events::{ - CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, + CommandExecuteData, ElicitationRequestedData, ExternalToolRequestedData, McpOauthRequiredData, SessionCanvasClosedData, SessionErrorData, SessionEventType, }; use crate::handler::{ AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, ExitPlanModeHandler, - PermissionHandler, PermissionResult, UserInputHandler, UserInputResponse, + McpAuthHandler, McpAuthRequest, McpAuthResult, PermissionHandler, PermissionResult, + UserInputHandler, UserInputResponse, }; use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; @@ -49,6 +52,7 @@ use crate::{ pub(crate) struct SessionHandlers { pub permission: Option>, pub elicitation: Option>, + pub mcp_auth: Option>, pub user_input: Option>, pub exit_plan_mode: Option>, pub auto_mode_switch: Option>, @@ -881,6 +885,7 @@ impl Client { let handlers = SessionHandlers { permission: permission_handler, elicitation: runtime.elicitation_handler.take(), + mcp_auth: runtime.mcp_auth_handler.take(), user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), @@ -895,6 +900,7 @@ impl Client { let canvas_handler = runtime.canvas_handler.take(); let session_fs_provider = runtime.session_fs_provider.take(); let bearer_token_providers = std::mem::take(&mut runtime.bearer_token_providers); + let has_mcp_auth_handler = handlers.mcp_auth.is_some(); if self.inner.session_fs_configured && session_fs_provider.is_none() { return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into()); } @@ -1030,6 +1036,9 @@ impl Client { "Client::create_session local setup complete" ); *capabilities.write() = create_result.capabilities.unwrap_or_default(); + if has_mcp_auth_handler { + register_mcp_auth_interest(self, &session_id).await?; + } tracing::debug!( elapsed_ms = total_start.elapsed().as_millis(), @@ -1139,6 +1148,7 @@ impl Client { let handlers = SessionHandlers { permission: permission_handler, elicitation: runtime.elicitation_handler.take(), + mcp_auth: runtime.mcp_auth_handler.take(), user_input: runtime.user_input_handler.take(), exit_plan_mode: runtime.exit_plan_mode_handler.take(), auto_mode_switch: runtime.auto_mode_switch_handler.take(), @@ -1153,6 +1163,7 @@ impl Client { let canvas_handler = runtime.canvas_handler.take(); let session_fs_provider = runtime.session_fs_provider.take(); let bearer_token_providers = std::mem::take(&mut runtime.bearer_token_providers); + let has_mcp_auth_handler = handlers.mcp_auth.is_some(); if self.inner.session_fs_configured && session_fs_provider.is_none() { return Err(ErrorKind::Session(SessionErrorKind::SessionFsProviderRequired).into()); } @@ -1170,6 +1181,9 @@ impl Client { let mut params = serde_json::to_value(&wire)?; let trace_ctx = self.resolve_trace_context().await; inject_trace_context(&mut params, &trace_ctx); + if has_mcp_auth_handler { + register_mcp_auth_interest(self, &session_id).await?; + } let capabilities = Arc::new(parking_lot::RwLock::new(SessionCapabilities::default())); let setup_start = Instant::now(); @@ -1477,6 +1491,17 @@ fn notification_permission_payload(result: &PermissionResult) -> Option { } } +async fn register_mcp_auth_interest(client: &Client, session_id: &SessionId) -> Result<(), Error> { + let mut params = serde_json::to_value(RegisterEventInterestParams { + event_type: "mcp.oauth_required".to_string(), + })?; + params["sessionId"] = Value::String(session_id.to_string()); + client + .call(rpc_methods::SESSION_EVENTLOG_REGISTERINTEREST, Some(params)) + .await?; + Ok(()) +} + fn tool_failure_result(message: impl Into) -> ToolResult { let message = message.into(); ToolResult::Expanded(ToolResultExpanded { @@ -1944,6 +1969,90 @@ async fn handle_notification( .instrument(span), ); } + SessionEventType::McpOauthRequired => { + let Some(request_id) = extract_request_id(¬ification.event.data) else { + return; + }; + let Some(mcp_auth_handler) = handlers.mcp_auth.clone() else { + warn!( + session_id = %session_id, + request_id = %request_id, + "received MCP OAuth request without a registered MCP auth handler" + ); + return; + }; + let data: McpOauthRequiredData = + match serde_json::from_value(notification.event.data.clone()) { + Ok(d) => d, + Err(e) => { + warn!(error = %e, "failed to deserialize MCP OAuth request"); + return; + } + }; + let request = McpAuthRequest { + server_name: data.server_name, + server_url: data.server_url, + reason: data.reason, + www_authenticate_params: data.www_authenticate_params, + resource_metadata: data.resource_metadata, + static_client_config: data.static_client_config, + }; + let client = client.clone(); + let sid = session_id.clone(); + let span = tracing::error_span!( + "mcp_auth_request_handler", + session_id = %sid, + request_id = %request_id + ); + tokio::spawn( + async move { + let cancel = McpAuthResult::Cancelled; + let handler_task = tokio::spawn({ + let sid = sid.clone(); + let request_id = request_id.clone(); + let span = tracing::error_span!( + "mcp_auth_callback", + session_id = %sid, + request_id = %request_id + ); + async move { + let handler_start = Instant::now(); + let response = mcp_auth_handler + .handle(sid.clone(), request_id.clone(), request) + .await; + tracing::debug!( + elapsed_ms = handler_start.elapsed().as_millis(), + session_id = %sid, + request_id = %request_id, + "McpAuthHandler::handle dispatch" + ); + response + } + .instrument(span) + }); + let result = match handler_task.await { + Ok(result) => result, + Err(_) => cancel, + }; + let rpc_start = Instant::now(); + let _ = client + .call( + "session.mcp.oauth.handlePendingRequest", + Some(serde_json::json!({ + "sessionId": sid, + "requestId": request_id, + "result": result.into_wire(), + })), + ) + .await; + tracing::debug!( + elapsed_ms = rpc_start.elapsed().as_millis(), + "Session::handle_notification MCP auth response sent" + ); + } + .instrument(span), + ); + } SessionEventType::CommandExecute => { let data: CommandExecuteData = match serde_json::from_value(notification.event.data.clone()) { diff --git a/rust/src/types.rs b/rust/src/types.rs index 75408db026..290937e392 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -24,8 +24,8 @@ use crate::generated::api_types::OpenCanvasInstance; pub use crate::generated::session_events::ContextTier; use crate::generated::session_events::ReasoningSummary; use crate::handler::{ - AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, PermissionHandler, - UserInputHandler, + AutoModeSwitchHandler, ElicitationHandler, ExitPlanModeHandler, McpAuthHandler, + PermissionHandler, UserInputHandler, }; use crate::hooks::SessionHooks; use crate::provider_token::BearerTokenProvider; @@ -1772,6 +1772,9 @@ pub struct SessionConfig { /// Optional elicitation-request handler. When `None`, /// `requestElicitation: false` goes on the wire. pub elicitation_handler: Option>, + /// Optional MCP OAuth request handler. When set, the SDK can satisfy MCP + /// server OAuth requests with host-acquired token data or cancellation. + pub mcp_auth_handler: Option>, /// Optional user-input handler. When `None`, /// `requestUserInput: false` goes on the wire and the `ask_user` /// tool is disabled. @@ -1901,6 +1904,10 @@ impl std::fmt::Debug for SessionConfig { "elicitation_handler", &self.elicitation_handler.as_ref().map(|_| ""), ) + .field( + "mcp_auth_handler", + &self.mcp_auth_handler.as_ref().map(|_| ""), + ) .field( "user_input_handler", &self.user_input_handler.as_ref().map(|_| ""), @@ -1990,6 +1997,7 @@ impl Default for SessionConfig { session_fs_provider: None, permission_handler: None, elicitation_handler: None, + mcp_auth_handler: None, user_input_handler: None, exit_plan_mode_handler: None, auto_mode_switch_handler: None, @@ -2013,6 +2021,7 @@ pub(crate) struct SessionConfigRuntime { pub permission_handler: Option>, pub permission_policy: Option, pub elicitation_handler: Option>, + pub mcp_auth_handler: Option>, pub user_input_handler: Option>, pub exit_plan_mode_handler: Option>, pub auto_mode_switch_handler: Option>, @@ -2143,6 +2152,7 @@ impl SessionConfig { permission_handler: self.permission_handler, permission_policy: self.permission_policy, elicitation_handler: self.elicitation_handler, + mcp_auth_handler: self.mcp_auth_handler, user_input_handler: self.user_input_handler, exit_plan_mode_handler: self.exit_plan_mode_handler, auto_mode_switch_handler: self.auto_mode_switch_handler, @@ -2173,6 +2183,12 @@ impl SessionConfig { self } + /// Install an [`McpAuthHandler`] for host-provided MCP OAuth tokens. + pub fn with_mcp_auth_handler(mut self, handler: Arc) -> Self { + self.mcp_auth_handler = Some(handler); + self + } + /// Install a [`UserInputHandler`]. Required for the `ask_user` tool /// to be enabled. pub fn with_user_input_handler(mut self, handler: Arc) -> Self { @@ -2851,6 +2867,8 @@ pub struct ResumeSessionConfig { /// Optional elicitation handler. See /// [`SessionConfig::elicitation_handler`]. pub elicitation_handler: Option>, + /// Optional MCP OAuth handler. See [`SessionConfig::mcp_auth_handler`]. + pub mcp_auth_handler: Option>, /// Optional user-input handler. See /// [`SessionConfig::user_input_handler`]. pub user_input_handler: Option>, @@ -3103,6 +3121,7 @@ impl ResumeSessionConfig { permission_handler: self.permission_handler, permission_policy: self.permission_policy, elicitation_handler: self.elicitation_handler, + mcp_auth_handler: self.mcp_auth_handler, user_input_handler: self.user_input_handler, exit_plan_mode_handler: self.exit_plan_mode_handler, auto_mode_switch_handler: self.auto_mode_switch_handler, @@ -3182,6 +3201,7 @@ impl ResumeSessionConfig { continue_pending_work: None, permission_handler: None, elicitation_handler: None, + mcp_auth_handler: None, user_input_handler: None, exit_plan_mode_handler: None, auto_mode_switch_handler: None, @@ -3207,6 +3227,12 @@ impl ResumeSessionConfig { self } + /// Install an [`McpAuthHandler`] for host-provided MCP OAuth tokens. + pub fn with_mcp_auth_handler(mut self, handler: Arc) -> Self { + self.mcp_auth_handler = Some(handler); + self + } + /// Install a [`UserInputHandler`] for the resumed session. pub fn with_user_input_handler(mut self, handler: Arc) -> Self { self.user_input_handler = Some(handler); diff --git a/rust/tests/e2e.rs b/rust/tests/e2e.rs index 59b83ab27c..79059c7f28 100644 --- a/rust/tests/e2e.rs +++ b/rust/tests/e2e.rs @@ -37,6 +37,8 @@ mod hooks; mod hooks_extended; #[path = "e2e/mcp_and_agents.rs"] mod mcp_and_agents; +#[path = "e2e/mcp_oauth.rs"] +mod mcp_oauth; #[path = "e2e/mode_empty.rs"] mod mode_empty; #[path = "e2e/mode_handlers.rs"] diff --git a/rust/tests/e2e/mcp_oauth.rs b/rust/tests/e2e/mcp_oauth.rs new file mode 100644 index 0000000000..f04ff0352a --- /dev/null +++ b/rust/tests/e2e/mcp_oauth.rs @@ -0,0 +1,430 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::process::Stdio; +use std::sync::Arc; + +use async_trait::async_trait; +use github_copilot_sdk::handler::{McpAuthHandler, McpAuthRequest, McpAuthResult}; +use github_copilot_sdk::rpc::{McpAppsCallToolRequest, McpListToolsRequest}; +use github_copilot_sdk::session::Session; +use github_copilot_sdk::session_events::{McpOauthRequestReason, McpServerStatus}; +use github_copilot_sdk::{McpHttpServerConfig, McpServerConfig, RequestId, SessionId}; +use parking_lot::Mutex; +use serde::Deserialize; +use serde_json::Value; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::process::{Child, Command}; + +use super::support::{wait_for_condition, with_e2e_context_no_snapshot}; + +const EXPECTED_TOKEN: &str = "sdk-host-token"; +const REFRESH_TOKEN: &str = "sdk-host-token-refresh"; +const UPSCOPE_TOKEN: &str = "sdk-host-token-upscope"; +const REAUTH_TOKEN: &str = "sdk-host-token-reauth"; + +#[tokio::test] +async fn should_satisfy_mcp_oauth_using_host_provided_token() { + with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let mut oauth_server = OAuthMcpServer::start( + ctx.repo_root() + .join("test/harness/test-mcp-oauth-server.mjs"), + ) + .await; + let server_name = "oauth-protected-mcp"; + let handler = Arc::new(TokenAuthHandler::default()); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_mcp_auth_handler(handler.clone()) + .with_mcp_servers(HashMap::from([( + server_name.to_string(), + McpServerConfig::Http(McpHttpServerConfig { + tools: Some(vec!["*".to_string()]), + timeout: None, + url: format!("{}/mcp", oauth_server.url), + headers: HashMap::new(), + }), + )])), + ) + .await + .expect("create session"); + + wait_for_mcp_server_status(&session, server_name, McpServerStatus::Connected).await; + let tools = session + .rpc() + .mcp() + .list_tools(McpListToolsRequest { + server_name: server_name.to_string(), + }) + .await + .expect("list MCP tools"); + assert!(tools.tools.iter().any(|tool| tool.name == "whoami")); + + let request = handler + .request + .lock() + .clone() + .expect("MCP auth handler should be invoked"); + assert_eq!(request.server_name, server_name); + assert_eq!(request.server_url, format!("{}/mcp", oauth_server.url)); + assert_eq!(request.reason, McpOauthRequestReason::Initial); + let www_authenticate = request + .www_authenticate_params + .expect("WWW-Authenticate params"); + assert_eq!( + www_authenticate.resource_metadata_url, + Some(format!( + "{}/.well-known/oauth-protected-resource", + oauth_server.url + )) + ); + assert_eq!(www_authenticate.scope.as_deref(), Some("mcp.read")); + assert_eq!(www_authenticate.error.as_deref(), Some("invalid_token")); + let metadata: Value = serde_json::from_str( + request + .resource_metadata + .as_deref() + .expect("resource metadata"), + ) + .expect("parse resource metadata"); + assert_eq!(metadata["resource"], format!("{}/mcp", oauth_server.url)); + + let requests = oauth_server.requests().await; + assert!( + requests + .iter() + .any(|request| request.authorization.is_none()) + ); + assert!( + requests.iter().any( + |request| request.authorization.as_deref() == Some("Bearer sdk-host-token") + ) + ); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + oauth_server.stop().await; + }) + }) + .await; +} + +#[tokio::test] +async fn should_request_replacement_tokens_across_mcp_oauth_lifecycle() { + with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let mut oauth_server = OAuthMcpServer::start( + ctx.repo_root() + .join("test/harness/test-mcp-oauth-server.mjs"), + ) + .await; + let server_name = "oauth-lifecycle-mcp"; + let handler = Arc::new(LifecycleAuthHandler::default()); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_enable_mcp_apps(true) + .with_mcp_auth_handler(handler.clone()) + .with_mcp_servers(HashMap::from([( + server_name.to_string(), + McpServerConfig::Http(McpHttpServerConfig { + tools: Some(vec!["*".to_string()]), + timeout: None, + url: format!("{}/mcp", oauth_server.url), + headers: HashMap::new(), + }), + )])), + ) + .await + .expect("create session"); + + wait_for_mcp_server_status(&session, server_name, McpServerStatus::Connected).await; + call_whoami(&session, server_name, "refresh").await; + call_whoami(&session, server_name, "upscope").await; + call_whoami(&session, server_name, "reauth").await; + + assert_eq!( + handler.reasons.lock().as_slice(), + [ + McpOauthRequestReason::Initial, + McpOauthRequestReason::Refresh, + McpOauthRequestReason::Upscope, + McpOauthRequestReason::Refresh, + McpOauthRequestReason::Reauth, + ] + ); + + let requests = oauth_server.requests().await; + assert!( + requests + .iter() + .any(|request| request.authorization.as_deref() + == Some("Bearer sdk-host-token-refresh")) + ); + assert!( + requests + .iter() + .any(|request| request.authorization.as_deref() + == Some("Bearer sdk-host-token-upscope")) + ); + assert!( + requests + .iter() + .any(|request| request.authorization.as_deref() + == Some("Bearer sdk-host-token-reauth")) + ); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + oauth_server.stop().await; + }) + }) + .await; +} + +#[tokio::test] +async fn should_cancel_pending_mcp_oauth_request() { + with_e2e_context_no_snapshot(|ctx| { + Box::pin(async move { + ctx.set_default_copilot_user(); + let mut oauth_server = OAuthMcpServer::start( + ctx.repo_root() + .join("test/harness/test-mcp-oauth-server.mjs"), + ) + .await; + let server_name = "oauth-cancelled-mcp"; + let handler = Arc::new(CancelAuthHandler::default()); + let client = ctx.start_client().await; + let session = client + .create_session( + ctx.approve_all_session_config() + .with_mcp_auth_handler(handler.clone()) + .with_mcp_servers(HashMap::from([( + server_name.to_string(), + McpServerConfig::Http(McpHttpServerConfig { + tools: Some(vec!["*".to_string()]), + timeout: None, + url: format!("{}/mcp", oauth_server.url), + headers: HashMap::new(), + }), + )])), + ) + .await + .expect("create session"); + + wait_for_mcp_server_status(&session, server_name, McpServerStatus::Failed).await; + + let request = handler + .request + .lock() + .clone() + .expect("MCP auth handler should be invoked"); + assert_eq!(request.server_name, server_name); + assert_eq!(request.reason, McpOauthRequestReason::Initial); + + session.disconnect().await.expect("disconnect session"); + client.stop().await.expect("stop client"); + oauth_server.stop().await; + }) + }) + .await; +} + +#[derive(Default)] +struct TokenAuthHandler { + request: Mutex>, +} + +#[async_trait] +impl McpAuthHandler for TokenAuthHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + request: McpAuthRequest, + ) -> McpAuthResult { + *self.request.lock() = Some(request); + McpAuthResult::Token { + access_token: EXPECTED_TOKEN.to_string(), + token_type: Some("Bearer".to_string()), + expires_in: Some(3600), + } + } +} + +#[derive(Default)] +struct LifecycleAuthHandler { + reasons: Mutex>, + refresh_count: Mutex, +} + +#[async_trait] +impl McpAuthHandler for LifecycleAuthHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + request: McpAuthRequest, + ) -> McpAuthResult { + let reason = request.reason.clone(); + self.reasons.lock().push(reason.clone()); + let token = match reason { + McpOauthRequestReason::Refresh => { + let www_authenticate = request + .www_authenticate_params + .as_ref() + .expect("refresh WWW-Authenticate params"); + assert_eq!(www_authenticate.resource_metadata_url, None); + assert_eq!(www_authenticate.error.as_deref(), Some("invalid_token")); + let mut refresh_count = self.refresh_count.lock(); + *refresh_count += 1; + if *refresh_count > 1 { + return McpAuthResult::Cancelled; + } + REFRESH_TOKEN + } + McpOauthRequestReason::Upscope => { + let www_authenticate = request + .www_authenticate_params + .as_ref() + .expect("upscope WWW-Authenticate params"); + assert!( + www_authenticate + .resource_metadata_url + .as_deref() + .is_some_and(|url| url.ends_with("/.well-known/oauth-protected-resource")) + ); + assert_eq!(www_authenticate.scope.as_deref(), Some("mcp.write")); + assert_eq!( + www_authenticate.error.as_deref(), + Some("insufficient_scope") + ); + UPSCOPE_TOKEN + } + McpOauthRequestReason::Reauth => REAUTH_TOKEN, + _ => EXPECTED_TOKEN, + }; + McpAuthResult::Token { + access_token: token.to_string(), + token_type: None, + expires_in: None, + } + } +} + +#[derive(Default)] +struct CancelAuthHandler { + request: Mutex>, +} + +#[async_trait] +impl McpAuthHandler for CancelAuthHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + request: McpAuthRequest, + ) -> McpAuthResult { + *self.request.lock() = Some(request); + McpAuthResult::Cancelled + } +} + +#[derive(Deserialize)] +struct OAuthMcpRequest { + authorization: Option, +} + +struct OAuthMcpServer { + child: Child, + url: String, +} + +impl OAuthMcpServer { + async fn start(script: PathBuf) -> Self { + let mut child = Command::new("node") + .arg(script) + .env("EXPECTED_TOKEN", EXPECTED_TOKEN) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true) + .spawn() + .expect("start OAuth MCP server"); + let stdout = child.stdout.take().expect("OAuth MCP stdout"); + let mut lines = BufReader::new(stdout).lines(); + let line = tokio::time::timeout(std::time::Duration::from_secs(10), lines.next_line()) + .await + .expect("OAuth MCP server startup timeout") + .expect("read OAuth MCP startup line") + .expect("OAuth MCP server stdout closed"); + let url = line + .strip_prefix("Listening: ") + .unwrap_or_else(|| panic!("unexpected OAuth MCP startup line: {line}")) + .to_string(); + Self { child, url } + } + + async fn requests(&self) -> Vec { + let text = reqwest::get(format!("{}/__requests", self.url)) + .await + .expect("fetch OAuth MCP requests") + .error_for_status() + .expect("OAuth MCP request status") + .text() + .await + .expect("read OAuth MCP requests"); + serde_json::from_str(&text).expect("decode OAuth MCP requests") + } + + async fn stop(&mut self) { + let _ = self.child.kill().await; + let _ = self.child.wait().await; + } +} + +async fn wait_for_mcp_server_status( + session: &Session, + server_name: &str, + expected_status: McpServerStatus, +) { + wait_for_condition("MCP server status", || async { + session + .rpc() + .mcp() + .list() + .await + .expect("list MCP servers") + .servers + .iter() + .any(|server| server.name == server_name && server.status == expected_status) + }) + .await; +} + +async fn call_whoami(session: &Session, server_name: &str, scenario: &str) { + let result = session + .rpc() + .mcp() + .apps() + .call_tool(McpAppsCallToolRequest { + arguments: Some(HashMap::from([( + "scenario".to_string(), + serde_json::Value::String(scenario.to_string()), + )])), + origin_server_name: server_name.to_string(), + server_name: server_name.to_string(), + tool_name: "whoami".to_string(), + }) + .await + .expect("call whoami"); + let content = result.get("content").expect("whoami content"); + assert_eq!( + content, + &serde_json::json!([{ "type": "text", "text": "oauth-test-user" }]) + ); +} diff --git a/rust/tests/e2e/support.rs b/rust/tests/e2e/support.rs index 1805eb145b..3c869a08b0 100644 --- a/rust/tests/e2e/support.rs +++ b/rust/tests/e2e/support.rs @@ -310,6 +310,8 @@ impl E2eContext { .as_os_str() .to_owned(), ), + ("COPILOT_MCP_APPS".into(), "true".into()), + ("MCP_APPS".into(), "true".into()), ]); if std::env::var("GITHUB_ACTIONS").as_deref() == Ok("true") { env.push(("GH_TOKEN".into(), "fake-token-for-e2e-tests".into())); @@ -598,6 +600,13 @@ fn cli_path(repo_root: &Path) -> std::io::Result { } } + let local_runtime_cli_path = PathBuf::from( + "/Users/roji/.copilot/repos/copilot-worktrees/copilot-agent-runtime/roji-symmetrical-dollop/dist-cli/index.js", + ); + if local_runtime_cli_path.exists() { + return Ok(local_runtime_cli_path); + } + // The `@github/copilot` package is a thin loader; the runnable `index.js` // ships in a platform-specific `@github/copilot--` package, // exactly one of which is installed. Resolve whichever one is present. diff --git a/rust/tests/session_test.rs b/rust/tests/session_test.rs index 98c6248230..31b0cc2330 100644 --- a/rust/tests/session_test.rs +++ b/rust/tests/session_test.rs @@ -9,17 +9,19 @@ use async_trait::async_trait; use github_copilot_sdk::canvas::{CanvasDeclaration, CanvasHandler, CanvasResult}; use github_copilot_sdk::handler::{ ApproveAllHandler, AutoModeSwitchHandler, AutoModeSwitchResponse, ElicitationHandler, - ExitPlanModeHandler, ExitPlanModeResult, UserInputHandler, UserInputResponse, + ExitPlanModeHandler, ExitPlanModeResult, McpAuthHandler, McpAuthRequest, McpAuthResult, + UserInputHandler, UserInputResponse, }; use github_copilot_sdk::rpc::{ CanvasProviderInvokeActionRequest, CanvasProviderOpenRequest, CanvasProviderOpenResult, OpenCanvasInstance, }; -use github_copilot_sdk::session_events::ReasoningSummary; +use github_copilot_sdk::session_events::{McpOauthRequiredData, ReasoningSummary}; use github_copilot_sdk::types::{ - CommandContext, CommandDefinition, CommandHandler, DeliveryMode, ElicitationRequest, - ElicitationResult, ExitPlanModeData, ExtensionInfo, MessageOptions, RequestId, SessionConfig, - SessionId, SetModelOptions, Tool, ToolInvocation, ToolResult, + CloudSessionOptions, CloudSessionRepository, CommandContext, CommandDefinition, CommandHandler, + DeliveryMode, ElicitationRequest, ElicitationResult, ExitPlanModeData, ExtensionInfo, + MessageOptions, RequestId, SessionConfig, SessionId, SetModelOptions, Tool, ToolInvocation, + ToolResult, }; use github_copilot_sdk::{Client, ContextTier, tool}; use serde_json::Value; @@ -30,6 +32,20 @@ const TIMEOUT: Duration = Duration::from_secs(2); struct TestCanvasHandler; +struct CancelMcpAuthHandler; + +#[async_trait] +impl McpAuthHandler for CancelMcpAuthHandler { + async fn handle( + &self, + _session_id: SessionId, + _request_id: RequestId, + _request: McpAuthRequest, + ) -> McpAuthResult { + McpAuthResult::Cancelled + } +} + #[async_trait] impl CanvasHandler for TestCanvasHandler { async fn on_open( @@ -220,12 +236,294 @@ fn rand_id() -> u64 { COUNTER.fetch_add(1, Ordering::Relaxed) as u64 } +#[test] +fn mcp_oauth_required_data_allows_optional_metadata() { + let with_metadata: McpOauthRequiredData = serde_json::from_value(serde_json::json!({ + "requestId": "oauth-request", + "reason": "initial", + "serverName": "oauth-server", + "serverUrl": "https://example.com/mcp", + "wwwAuthenticateParams": { + "resourceMetadataUrl": "https://example.com/.well-known/oauth-protected-resource" + }, + "resourceMetadata": "{\"resource\":\"https://example.com/mcp\"}", + "staticClientConfig": { + "clientId": "static-client", + "clientSecret": "static-secret", + "publicClient": false + } + })) + .unwrap(); + assert_eq!( + with_metadata.resource_metadata.as_deref(), + Some("{\"resource\":\"https://example.com/mcp\"}") + ); + assert!(with_metadata.www_authenticate_params.is_some()); + assert_eq!( + with_metadata + .static_client_config + .as_ref() + .and_then(|config| config.client_secret.as_deref()), + Some("static-secret") + ); + + let without_metadata: McpOauthRequiredData = serde_json::from_value(serde_json::json!({ + "requestId": "oauth-request", + "reason": "initial", + "serverName": "oauth-server", + "serverUrl": "https://example.com/mcp" + })) + .unwrap(); + assert!(without_metadata.resource_metadata.is_none()); + assert!(without_metadata.www_authenticate_params.is_none()); +} + fn requested_session_id(request: &Value) -> &str { request["params"]["sessionId"] .as_str() .expect("session request should include sessionId") } +#[tokio::test] +async fn create_session_registers_mcp_auth_interest_only_with_handler() { + let (client, mut server_read, mut server_write) = make_client(); + let create_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default().with_permission_handler(Arc::new(ApproveAllHandler)), + ) + .await + .unwrap() + } + }); + + let create_req = read_framed(&mut server_read).await; + assert_eq!(create_req["method"], "session.create"); + assert_eq!(create_req["params"]["requestPermission"], true); + let session_id = requested_session_id(&create_req).to_string(); + server_respond_create(&mut server_write, &create_req, &session_id).await; + let session = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); + + let no_extra_request = timeout(Duration::from_millis(50), read_framed(&mut server_read)).await; + assert!(no_extra_request.is_err()); + drop(session); + + let (client, mut server_read, mut server_write) = make_client(); + let create_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_mcp_auth_handler(Arc::new(CancelMcpAuthHandler)), + ) + .await + .unwrap() + } + }); + + let create_req = read_framed(&mut server_read).await; + assert_eq!(create_req["method"], "session.create"); + assert_eq!(create_req["params"]["requestPermission"], true); + let session_id = requested_session_id(&create_req).to_string(); + server_respond_create(&mut server_write, &create_req, &session_id).await; + + let interest_req = read_framed(&mut server_read).await; + assert_eq!(interest_req["method"], "session.eventLog.registerInterest"); + assert_eq!(interest_req["params"]["eventType"], "mcp.oauth_required"); + let id = interest_req["id"].as_u64().unwrap(); + write_framed( + &mut server_write, + &serde_json::to_vec(&serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { "id": "interest-1" }, + })) + .unwrap(), + ) + .await; + + let _session = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn cloud_create_session_registers_mcp_auth_interest_after_create_only_with_handler() { + let cloud = || { + CloudSessionOptions::with_repository( + CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"), + ) + }; + + let (client, mut server_read, mut server_write) = make_client(); + let create_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_cloud(cloud()), + ) + .await + .unwrap() + } + }); + + let create_req = read_framed(&mut server_read).await; + assert_eq!(create_req["method"], "session.create"); + assert!(create_req["params"].get("sessionId").is_none()); + assert_eq!(create_req["params"]["requestPermission"], true); + server_respond_create(&mut server_write, &create_req, "server-assigned-session-1").await; + let session = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); + let no_extra_request = timeout(Duration::from_millis(50), read_framed(&mut server_read)).await; + assert!(no_extra_request.is_err()); + drop(session); + + let (client, mut server_read, mut server_write) = make_client(); + let create_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .create_session( + SessionConfig::default() + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_mcp_auth_handler(Arc::new(CancelMcpAuthHandler)) + .with_cloud(cloud()), + ) + .await + .unwrap() + } + }); + + let create_req = read_framed(&mut server_read).await; + assert_eq!(create_req["method"], "session.create"); + assert!(create_req["params"].get("sessionId").is_none()); + assert_eq!(create_req["params"]["requestPermission"], true); + server_respond_create(&mut server_write, &create_req, "server-assigned-session-2").await; + + let interest_req = read_framed(&mut server_read).await; + assert_eq!(interest_req["method"], "session.eventLog.registerInterest"); + assert_eq!( + interest_req["params"]["sessionId"], + "server-assigned-session-2" + ); + assert_eq!(interest_req["params"]["eventType"], "mcp.oauth_required"); + let id = interest_req["id"].as_u64().unwrap(); + write_framed( + &mut server_write, + &serde_json::to_vec(&serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { "id": "interest-1" }, + })) + .unwrap(), + ) + .await; + let _session = timeout(TIMEOUT, create_handle).await.unwrap().unwrap(); +} + +#[tokio::test] +async fn resume_session_registers_mcp_auth_interest_only_with_handler() { + use github_copilot_sdk::types::ResumeSessionConfig; + + let (client, mut server_read, mut server_write) = make_client(); + let resume_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(SessionId::from("session-without-auth")) + .with_permission_handler(Arc::new(ApproveAllHandler)), + ) + .await + .unwrap() + } + }); + + let resume_req = read_framed(&mut server_read).await; + assert_eq!(resume_req["method"], "session.resume"); + assert_eq!(resume_req["params"]["requestPermission"], true); + server_respond_create(&mut server_write, &resume_req, "session-without-auth").await; + respond_to_reload(&mut server_read, &mut server_write).await; + let session = timeout(TIMEOUT, resume_handle).await.unwrap().unwrap(); + let no_extra_request = timeout(Duration::from_millis(50), read_framed(&mut server_read)).await; + assert!(no_extra_request.is_err()); + drop(session); + + let (client, mut server_read, mut server_write) = make_client(); + let resume_handle = tokio::spawn({ + let client = client.clone(); + async move { + client + .resume_session( + ResumeSessionConfig::new(SessionId::from("session-with-auth")) + .with_permission_handler(Arc::new(ApproveAllHandler)) + .with_mcp_auth_handler(Arc::new(CancelMcpAuthHandler)), + ) + .await + .unwrap() + } + }); + + let interest_req = read_framed(&mut server_read).await; + assert_eq!(interest_req["method"], "session.eventLog.registerInterest"); + assert_eq!(interest_req["params"]["eventType"], "mcp.oauth_required"); + let id = interest_req["id"].as_u64().unwrap(); + write_framed( + &mut server_write, + &serde_json::to_vec(&serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { "id": "interest-1" }, + })) + .unwrap(), + ) + .await; + + let resume_req = read_framed(&mut server_read).await; + assert_eq!(resume_req["method"], "session.resume"); + assert_eq!(resume_req["params"]["requestPermission"], true); + server_respond_create(&mut server_write, &resume_req, "session-with-auth").await; + respond_to_reload(&mut server_read, &mut server_write).await; + let _session = timeout(TIMEOUT, resume_handle).await.unwrap().unwrap(); +} + +async fn server_respond_create( + writer: &mut (impl AsyncWrite + Unpin), + request: &Value, + session_id: &str, +) { + let id = request["id"].as_u64().unwrap(); + write_framed( + writer, + &serde_json::to_vec(&serde_json::json!({ + "jsonrpc": "2.0", + "id": id, + "result": { "sessionId": session_id, "workspacePath": "/tmp/workspace" }, + })) + .unwrap(), + ) + .await; +} + +async fn respond_to_reload( + reader: &mut (impl tokio::io::AsyncRead + Unpin), + writer: &mut (impl AsyncWrite + Unpin), +) { + let reload = read_framed(reader).await; + assert_eq!(reload["method"], "session.skills.reload"); + let id = reload["id"].as_u64().unwrap(); + write_framed( + writer, + &serde_json::to_vec(&serde_json::json!({ "jsonrpc": "2.0", "id": id, "result": {} })) + .unwrap(), + ) + .await; +} + #[tokio::test] async fn session_subscribe_yields_events_observe_only() { let (session, mut server) = create_session_pair().await; diff --git a/test/harness/test-mcp-oauth-server.mjs b/test/harness/test-mcp-oauth-server.mjs new file mode 100644 index 0000000000..fdb2047ec6 --- /dev/null +++ b/test/harness/test-mcp-oauth-server.mjs @@ -0,0 +1,305 @@ +#!/usr/bin/env node +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +/** + * Minimal OAuth-protected Streamable HTTP MCP server for SDK E2E tests. + * + * The `/mcp` endpoint returns a WWW-Authenticate challenge until requests include + * an accepted test token, then serves enough JSON-RPC MCP methods for the runtime + * to initialize and list/call one tool. Specific tool-call scenarios trigger + * replacement-token challenges so SDK E2E tests can cover refresh, upscope, and + * reauth flows without relying on a real OAuth server. + */ + +import http from "node:http"; + +const DEFAULT_EXPECTED_TOKEN = "sdk-host-token"; +const PROTOCOL_VERSION = "2025-03-26"; +const PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource"; + +export async function startOAuthMcpServer({ + expectedToken = DEFAULT_EXPECTED_TOKEN, + host = "127.0.0.1", + port = 0, +} = {}) { + const requests = []; + const tokens = { + initial: expectedToken, + refresh: `${expectedToken}-refresh`, + upscope: `${expectedToken}-upscope`, + reauth: `${expectedToken}-reauth`, + rejected: `${expectedToken}-rejected`, + }; + const acceptedTokens = new Set([ + tokens.initial, + tokens.refresh, + tokens.upscope, + tokens.reauth, + ]); + + const server = http.createServer(async (req, res) => { + const url = new URL( + req.url ?? "/", + `http://${req.headers.host ?? `${host}:${port}`}`, + ); + const baseUrl = `http://${req.headers.host}`; + + if (req.method === "GET" && url.pathname === "/__requests") { + respondJson(res, 200, requests); + return; + } + + if ( + req.method === "GET" && + url.pathname === PROTECTED_RESOURCE_PATH + ) { + respondJson(res, 200, { + resource: `${baseUrl}/mcp`, + authorization_servers: [baseUrl], + scopes_supported: ["mcp.read"], + bearer_methods_supported: ["header"], + }); + return; + } + + if ( + req.method === "GET" && + url.pathname === "/.well-known/oauth-authorization-server" + ) { + respondJson(res, 200, { + issuer: baseUrl, + authorization_endpoint: `${baseUrl}/authorize`, + token_endpoint: `${baseUrl}/token`, + response_types_supported: ["code"], + grant_types_supported: ["authorization_code"], + }); + return; + } + + if (url.pathname !== "/mcp") { + respondJson(res, 404, { error: "not_found" }); + return; + } + + const body = await readBody(req); + requests.push({ + method: req.method, + path: url.pathname, + authorization: req.headers.authorization ?? null, + body: body ? JSON.parse(body) : null, + }); + + const token = parseBearerToken(req.headers.authorization); + if (!token || !acceptedTokens.has(token)) { + challengeInitial(res, baseUrl); + return; + } + + if (req.method !== "POST") { + respondJson(res, 405, { error: "method_not_allowed" }); + return; + } + + const message = body ? JSON.parse(body) : undefined; + const replacementChallenge = getReplacementChallenge( + message, + token, + tokens, + baseUrl, + ); + if (replacementChallenge) { + res.writeHead(replacementChallenge.statusCode, { + "www-authenticate": replacementChallenge.wwwAuthenticate, + "content-type": "application/json", + }); + res.end(JSON.stringify({ error: replacementChallenge.error })); + return; + } + + const response = Array.isArray(message) + ? message + .map((item) => handleJsonRpcMessage(item)) + .filter((item) => item !== undefined) + : handleJsonRpcMessage(message); + + if ( + response === undefined || + (Array.isArray(response) && response.length === 0) + ) { + res.writeHead(202, { "mcp-session-id": "oauth-test-session" }); + res.end(); + return; + } + + res.writeHead(200, { + "content-type": "application/json", + "mcp-session-id": "oauth-test-session", + }); + res.end(JSON.stringify(response)); + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(port, host, () => { + server.off("error", reject); + resolve(); + }); + }); + + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Expected TCP server address"); + } + + return { + url: `http://${host}:${address.port}`, + requests, + close: () => + new Promise((resolve, reject) => + server.close((err) => (err ? reject(err) : resolve())), + ), + }; +} + +function getReplacementChallenge(message, token, tokens, baseUrl) { + const messages = Array.isArray(message) ? message : [message]; + const toolCall = messages.find((item) => item?.method === "tools/call"); + const scenario = toolCall?.params?.arguments?.scenario; + + if (scenario === "refresh" && token !== tokens.refresh) { + return { + statusCode: 401, + wwwAuthenticate: 'Bearer error="invalid_token"', + error: "token_expired", + }; + } + + if (scenario === "upscope" && token !== tokens.upscope) { + return { + statusCode: 403, + wwwAuthenticate: `Bearer resource_metadata="${baseUrl}${PROTECTED_RESOURCE_PATH}", scope="mcp.write", error="insufficient_scope"`, + error: "insufficient_scope", + }; + } + + if (scenario === "reauth" && token !== tokens.reauth) { + return { + statusCode: 401, + wwwAuthenticate: 'Bearer error="invalid_token"', + error: "reauth_required", + }; + } + + if (scenario === "cancel" && token !== tokens.refresh) { + return { + statusCode: 401, + wwwAuthenticate: 'Bearer error="invalid_token"', + error: "token_expired", + }; + } + + return undefined; +} + +function handleJsonRpcMessage(message) { + if (!message || typeof message !== "object" || !("id" in message)) { + return undefined; + } + + switch (message.method) { + case "initialize": + return { + jsonrpc: "2.0", + id: message.id, + result: { + protocolVersion: message.params?.protocolVersion ?? PROTOCOL_VERSION, + capabilities: { tools: {} }, + serverInfo: { name: "oauth-test-server", version: "1.0.0" }, + }, + }; + case "tools/list": + return { + jsonrpc: "2.0", + id: message.id, + result: { + tools: [ + { + name: "whoami", + description: "Returns the authenticated test principal.", + inputSchema: { + type: "object", + properties: { + scenario: { + type: "string", + enum: ["initial", "refresh", "upscope", "reauth", "cancel"], + }, + }, + additionalProperties: false, + }, + _meta: { "ui.visibility": ["model", "app"] }, + }, + ], + }, + }; + case "tools/call": + return { + jsonrpc: "2.0", + id: message.id, + result: { + content: [{ type: "text", text: "oauth-test-user" }], + isError: false, + }, + }; + default: + return { + jsonrpc: "2.0", + id: message.id, + error: { code: -32601, message: `Method not found: ${message.method}` }, + }; + } +} + +function parseBearerToken(authorization) { + const match = /^Bearer (.+)$/.exec(authorization ?? ""); + return match?.[1]; +} + +function challengeInitial(res, baseUrl) { + const resourceMetadataUrl = `${baseUrl}${PROTECTED_RESOURCE_PATH}`; + res.writeHead(401, { + "www-authenticate": `Bearer resource_metadata="${resourceMetadataUrl}", scope="mcp.read", error="invalid_token"`, + "content-type": "application/json", + }); + res.end(JSON.stringify({ error: "missing_or_invalid_token" })); +} + +function readBody(req) { + return new Promise((resolve, reject) => { + const chunks = []; + req.on("data", (chunk) => chunks.push(chunk)); + req.on("error", reject); + req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); + }); +} + +function respondJson(res, statusCode, body) { + const data = JSON.stringify(body); + res.writeHead(statusCode, { + "content-type": "application/json", + "content-length": Buffer.byteLength(data), + }); + res.end(data); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const server = await startOAuthMcpServer({ + expectedToken: process.env.EXPECTED_TOKEN ?? DEFAULT_EXPECTED_TOKEN, + }); + console.log(`Listening: ${server.url}`); + process.on("SIGTERM", async () => { + await server.close(); + process.exit(0); + }); +} From f68b9088d987a1cc27344e8977f20f168961fdef Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:47:55 +0000 Subject: [PATCH 2/4] Regenerate Java codegen output Auto-committed by java-codegen-check workflow. --- .../copilot/generated/AssistantIdleEvent.java | 41 ---------------- .../McpHeadersRefreshCompletedEvent.java | 43 ---------------- .../McpHeadersRefreshCompletedOutcome.java | 37 -------------- .../McpHeadersRefreshRequiredEvent.java | 47 ------------------ .../McpHeadersRefreshRequiredReason.java | 37 -------------- .../generated/McpOauthRequestReason.java | 39 --------------- .../generated/McpOauthRequiredEvent.java | 4 +- .../McpOauthRequiredStaticClientConfig.java | 2 - .../McpOauthWWWAuthenticateParams.java | 2 +- .../generated/ResponseBudgetConfig.java | 29 ----------- .../copilot/generated/SessionEvent.java | 6 --- .../copilot/generated/SessionResumeEvent.java | 2 - .../copilot/generated/SessionStartEvent.java | 2 - .../generated/ToolExecutionStartEvent.java | 2 - .../ToolExecutionStartShellToolInfo.java | 30 ------------ .../generated/UserMessageDelivery.java | 37 -------------- .../copilot/generated/UserMessageEvent.java | 2 - .../copilot/generated/rpc/ModelBilling.java | 4 +- .../generated/rpc/ResponseBudgetConfig.java | 29 ----------- .../copilot/generated/rpc/SessionMcpApi.java | 3 -- .../generated/rpc/SessionMcpHeadersApi.java | 49 ------------------- ...dlePendingHeadersRefreshRequestParams.java | 34 ------------- ...dlePendingHeadersRefreshRequestResult.java | 30 ------------ .../rpc/SessionOptionsUpdateParams.java | 6 +-- ...sionToolsUpdateSubagentSettingsParams.java | 6 +-- .../rpc/SlashCommandInvocationResult.java | 2 +- 26 files changed, 6 insertions(+), 519 deletions(-) delete mode 100644 java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java diff --git a/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java b/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java deleted file mode 100644 index 3b79b8d50e..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java +++ /dev/null @@ -1,41 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Session event "assistant.idle". Payload emitted whenever the main agent's processing loop goes idle, including while related background work (running agents or in-flight attached shell commands) is still pending and the session-level idle event is therefore deferred - * @since 1.0.0 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class AssistantIdleEvent extends SessionEvent { - - @Override - public String getType() { return "assistant.idle"; } - - @JsonProperty("data") - private AssistantIdleEventData data; - - public AssistantIdleEventData getData() { return data; } - public void setData(AssistantIdleEventData data) { this.data = data; } - - /** Data payload for {@link AssistantIdleEvent}. */ - @JsonIgnoreProperties(ignoreUnknown = true) - @JsonInclude(JsonInclude.Include.NON_NULL) - public record AssistantIdleEventData( - /** True when the preceding agentic loop was cancelled via abort signal */ - @JsonProperty("aborted") Boolean aborted - ) { - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java deleted file mode 100644 index a3ba903aea..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Session event "mcp.headers_refresh_completed". MCP headers refresh request completion notification - * @since 1.0.0 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class McpHeadersRefreshCompletedEvent extends SessionEvent { - - @Override - public String getType() { return "mcp.headers_refresh_completed"; } - - @JsonProperty("data") - private McpHeadersRefreshCompletedEventData data; - - public McpHeadersRefreshCompletedEventData getData() { return data; } - public void setData(McpHeadersRefreshCompletedEventData data) { this.data = data; } - - /** Data payload for {@link McpHeadersRefreshCompletedEvent}. */ - @JsonIgnoreProperties(ignoreUnknown = true) - @JsonInclude(JsonInclude.Include.NON_NULL) - public record McpHeadersRefreshCompletedEventData( - /** Request ID of the resolved headers refresh request */ - @JsonProperty("requestId") String requestId, - /** How the pending MCP headers refresh request resolved. */ - @JsonProperty("outcome") McpHeadersRefreshCompletedOutcome outcome - ) { - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java deleted file mode 100644 index 7980dd0a67..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import javax.annotation.processing.Generated; - -/** - * How the pending MCP headers refresh request resolved. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum McpHeadersRefreshCompletedOutcome { - /** The {@code headers} variant. */ - HEADERS("headers"), - /** The {@code none} variant. */ - NONE("none"), - /** The {@code timeout} variant. */ - TIMEOUT("timeout"); - - private final String value; - McpHeadersRefreshCompletedOutcome(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static McpHeadersRefreshCompletedOutcome fromValue(String value) { - for (McpHeadersRefreshCompletedOutcome v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown McpHeadersRefreshCompletedOutcome value: " + value); - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java deleted file mode 100644 index d8774bb326..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java +++ /dev/null @@ -1,47 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Session event "mcp.headers_refresh_required". Dynamic headers refresh request for a remote MCP server - * @since 1.0.0 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class McpHeadersRefreshRequiredEvent extends SessionEvent { - - @Override - public String getType() { return "mcp.headers_refresh_required"; } - - @JsonProperty("data") - private McpHeadersRefreshRequiredEventData data; - - public McpHeadersRefreshRequiredEventData getData() { return data; } - public void setData(McpHeadersRefreshRequiredEventData data) { this.data = data; } - - /** Data payload for {@link McpHeadersRefreshRequiredEvent}. */ - @JsonIgnoreProperties(ignoreUnknown = true) - @JsonInclude(JsonInclude.Include.NON_NULL) - public record McpHeadersRefreshRequiredEventData( - /** Unique identifier for this headers refresh request; used to respond via session.mcp.headers.handlePendingHeadersRefreshRequest() */ - @JsonProperty("requestId") String requestId, - /** Display name of the remote MCP server requesting headers */ - @JsonProperty("serverName") String serverName, - /** URL of the remote MCP server requesting headers */ - @JsonProperty("serverUrl") String serverUrl, - /** Why dynamic headers are being requested. */ - @JsonProperty("reason") McpHeadersRefreshRequiredReason reason - ) { - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java deleted file mode 100644 index 86c8f8b2d6..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import javax.annotation.processing.Generated; - -/** - * Why dynamic headers are being requested. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum McpHeadersRefreshRequiredReason { - /** The {@code startup} variant. */ - STARTUP("startup"), - /** The {@code ttl-expired} variant. */ - TTL_EXPIRED("ttl-expired"), - /** The {@code auth-failed} variant. */ - AUTH_FAILED("auth-failed"); - - private final String value; - McpHeadersRefreshRequiredReason(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static McpHeadersRefreshRequiredReason fromValue(String value) { - for (McpHeadersRefreshRequiredReason v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown McpHeadersRefreshRequiredReason value: " + value); - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java deleted file mode 100644 index 2a6eec7063..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java +++ /dev/null @@ -1,39 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import javax.annotation.processing.Generated; - -/** - * Reason the runtime is requesting host-provided MCP OAuth credentials - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum McpOauthRequestReason { - /** The {@code initial} variant. */ - INITIAL("initial"), - /** The {@code refresh} variant. */ - REFRESH("refresh"), - /** The {@code reauth} variant. */ - REAUTH("reauth"), - /** The {@code upscope} variant. */ - UPSCOPE("upscope"); - - private final String value; - McpOauthRequestReason(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static McpOauthRequestReason fromValue(String value) { - for (McpOauthRequestReason v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown McpOauthRequestReason value: " + value); - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java index 67413d382f..3f6ef8ef60 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java @@ -45,9 +45,7 @@ public record McpOauthRequiredEventData( /** OAuth WWW-Authenticate parameters parsed from the auth challenge, if available */ @JsonProperty("wwwAuthenticateParams") McpOauthWWWAuthenticateParams wwwAuthenticateParams, /** Raw OAuth protected-resource metadata document fetched for the MCP server, if available */ - @JsonProperty("resourceMetadata") String resourceMetadata, - /** Why the runtime is requesting host-provided OAuth credentials. */ - @JsonProperty("reason") McpOauthRequestReason reason + @JsonProperty("resourceMetadata") String resourceMetadata ) { } } diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java index 5f42ec90c4..764f8b7fc8 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java @@ -23,8 +23,6 @@ public record McpOauthRequiredStaticClientConfig( /** OAuth client ID for the server */ @JsonProperty("clientId") String clientId, - /** Optional OAuth client secret for confidential static clients, when the runtime can resolve one */ - @JsonProperty("clientSecret") String clientSecret, /** Whether this is a public OAuth client */ @JsonProperty("publicClient") Boolean publicClient, /** Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). */ diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java b/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java index 3e1fdb0d10..faa08e1e89 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record McpOauthWWWAuthenticateParams( - /** Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter, if present */ + /** Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter */ @JsonProperty("resourceMetadataUrl") String resourceMetadataUrl, /** Requested OAuth scopes from the WWW-Authenticate scope parameter, if present */ @JsonProperty("scope") String scope, diff --git a/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java b/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java deleted file mode 100644 index 0bc290e2df..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Optional experimental response budget limits. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record ResponseBudgetConfig( - /** Maximum model-call iterations allowed while responding to one top-level user message. */ - @JsonProperty("maxModelIterations") Long maxModelIterations, - /** Maximum AI Credits allowed while responding to one top-level user message. */ - @JsonProperty("maxAiCredits") Double maxAiCredits -) { -} diff --git a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java index bcbb09831c..f92dbf2fae 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -63,7 +63,6 @@ @JsonSubTypes.Type(value = AssistantMessageStartEvent.class, name = "assistant.message_start"), @JsonSubTypes.Type(value = AssistantMessageDeltaEvent.class, name = "assistant.message_delta"), @JsonSubTypes.Type(value = AssistantTurnEndEvent.class, name = "assistant.turn_end"), - @JsonSubTypes.Type(value = AssistantIdleEvent.class, name = "assistant.idle"), @JsonSubTypes.Type(value = AssistantUsageEvent.class, name = "assistant.usage"), @JsonSubTypes.Type(value = ModelCallFailureEvent.class, name = "model.call_failure"), @JsonSubTypes.Type(value = AbortEvent.class, name = "abort"), @@ -94,8 +93,6 @@ @JsonSubTypes.Type(value = SamplingCompletedEvent.class, name = "sampling.completed"), @JsonSubTypes.Type(value = McpOauthRequiredEvent.class, name = "mcp.oauth_required"), @JsonSubTypes.Type(value = McpOauthCompletedEvent.class, name = "mcp.oauth_completed"), - @JsonSubTypes.Type(value = McpHeadersRefreshRequiredEvent.class, name = "mcp.headers_refresh_required"), - @JsonSubTypes.Type(value = McpHeadersRefreshCompletedEvent.class, name = "mcp.headers_refresh_completed"), @JsonSubTypes.Type(value = SessionCustomNotificationEvent.class, name = "session.custom_notification"), @JsonSubTypes.Type(value = ExternalToolRequestedEvent.class, name = "external_tool.requested"), @JsonSubTypes.Type(value = ExternalToolCompletedEvent.class, name = "external_tool.completed"), @@ -164,7 +161,6 @@ public abstract sealed class SessionEvent permits AssistantMessageStartEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, - AssistantIdleEvent, AssistantUsageEvent, ModelCallFailureEvent, AbortEvent, @@ -195,8 +191,6 @@ public abstract sealed class SessionEvent permits SamplingCompletedEvent, McpOauthRequiredEvent, McpOauthCompletedEvent, - McpHeadersRefreshRequiredEvent, - McpHeadersRefreshCompletedEvent, SessionCustomNotificationEvent, ExternalToolRequestedEvent, ExternalToolCompletedEvent, diff --git a/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java index d4c2978777..b1cfda9338 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java @@ -49,8 +49,6 @@ public record SessionResumeEventData( @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Context tier currently selected at resume time; null when no tier is active */ @JsonProperty("contextTier") ContextTier contextTier, - /** Experimental response budget limits currently configured at resume time; null when no budget is active */ - @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget, /** Updated working directory and git context at resume time */ @JsonProperty("context") WorkingDirectoryContext context, /** Whether the session was already in use by another client at resume time */ diff --git a/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java index 6d2921ff05..2b36fa3e7b 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java @@ -53,8 +53,6 @@ public record SessionStartEventData( @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) */ @JsonProperty("contextTier") ContextTier contextTier, - /** Experimental response budget limits configured at session creation time, if any */ - @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget, /** Working directory and git context at session start */ @JsonProperty("context") WorkingDirectoryContext context, /** Whether the session was already in use by another client at start time */ diff --git a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java index b119a3eb0e..cd4e5a137d 100644 --- a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java @@ -40,8 +40,6 @@ public record ToolExecutionStartEventData( @JsonProperty("toolName") String toolName, /** Arguments passed to the tool */ @JsonProperty("arguments") Object arguments, - /** Shell-tool path hints derived from the command at start time for shell tools (bash/powershell/local_shell). Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. Absent for non-shell tools. */ - @JsonProperty("shellToolInfo") ToolExecutionStartShellToolInfo shellToolInfo, /** Model identifier that generated this tool call */ @JsonProperty("model") String model, /** Name of the MCP server hosting this tool, when the tool is an MCP tool */ diff --git a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java deleted file mode 100644 index 88ac787fca..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import javax.annotation.processing.Generated; - -/** - * Shell-aware path hints for a shell tool's command, captured at start time so consumers can snapshot a file's pre-image before the tool runs. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record ToolExecutionStartShellToolInfo( - /** File paths the command may read or write, derived from the command at start time. Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. */ - @JsonProperty("possiblePaths") List possiblePaths, - /** Whether the command includes a file write redirection (e.g., > or >>). */ - @JsonProperty("hasWriteFileRedirection") Boolean hasWriteFileRedirection -) { -} diff --git a/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java b/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java deleted file mode 100644 index ab64a88592..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import javax.annotation.processing.Generated; - -/** - * How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum UserMessageDelivery { - /** The {@code idle} variant. */ - IDLE("idle"), - /** The {@code steering} variant. */ - STEERING("steering"), - /** The {@code queued} variant. */ - QUEUED("queued"); - - private final String value; - UserMessageDelivery(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static UserMessageDelivery fromValue(String value) { - for (UserMessageDelivery v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown UserMessageDelivery value: " + value); - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java b/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java index 57f64b6527..69f959ab65 100644 --- a/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java @@ -47,8 +47,6 @@ public record UserMessageEventData( @JsonProperty("nativeDocumentPathFallbackPaths") List nativeDocumentPathFallbackPaths, /** Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user) */ @JsonProperty("source") String source, - /** How this message was delivered to the agentic loop relative to loop state (idle-start vs. steering/queued while busy). The timing axis; combine with `source` (origin) for the full picture. Used for telemetry attribution. */ - @JsonProperty("delivery") UserMessageDelivery delivery, /** The agent mode that was active when this message was sent */ @JsonProperty("agentMode") UserMessageAgentMode agentMode, /** True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. */ diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java index 53ed64f4bf..94a8188f16 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java @@ -24,8 +24,6 @@ public record ModelBilling( /** Billing cost multiplier relative to the base rate */ @JsonProperty("multiplier") Double multiplier, /** Token-level pricing information for this model */ - @JsonProperty("tokenPrices") ModelBillingTokenPrices tokenPrices, - /** Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. */ - @JsonProperty("discountPercent") Long discountPercent + @JsonProperty("tokenPrices") ModelBillingTokenPrices tokenPrices ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java b/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java deleted file mode 100644 index 4cbe89b4be..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Optional experimental response budget limits. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record ResponseBudgetConfig( - /** Maximum model-call iterations allowed while responding to one top-level user message. */ - @JsonProperty("maxModelIterations") Long maxModelIterations, - /** Maximum AI Credits allowed while responding to one top-level user message. */ - @JsonProperty("maxAiCredits") Double maxAiCredits -) { -} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java index f4603c249f..4246b11f82 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java @@ -26,8 +26,6 @@ public final class SessionMcpApi { /** API methods for the {@code mcp.oauth} sub-namespace. */ public final SessionMcpOauthApi oauth; - /** API methods for the {@code mcp.headers} sub-namespace. */ - public final SessionMcpHeadersApi headers; /** API methods for the {@code mcp.apps} sub-namespace. */ public final SessionMcpAppsApi apps; @@ -36,7 +34,6 @@ public final class SessionMcpApi { this.caller = caller; this.sessionId = sessionId; this.oauth = new SessionMcpOauthApi(caller, sessionId); - this.headers = new SessionMcpHeadersApi(caller, sessionId); this.apps = new SessionMcpAppsApi(caller, sessionId); } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java deleted file mode 100644 index 45679f83a4..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java +++ /dev/null @@ -1,49 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.github.copilot.CopilotExperimental; -import java.util.concurrent.CompletableFuture; -import javax.annotation.processing.Generated; - -/** - * API methods for the {@code mcp.headers} namespace. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class SessionMcpHeadersApi { - - private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; - - private final RpcCaller caller; - private final String sessionId; - - /** @param caller the RPC transport function */ - SessionMcpHeadersApi(RpcCaller caller, String sessionId) { - this.caller = caller; - this.sessionId = sessionId; - } - - /** - * MCP headers refresh request id and the host response. - *

- * Note: the {@code sessionId} field in the params record is overridden - * by the session-scoped wrapper; any value provided is ignored. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ - @CopilotExperimental - public CompletableFuture handlePendingHeadersRefreshRequest(SessionMcpHeadersHandlePendingHeadersRefreshRequestParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.mcp.headers.handlePendingHeadersRefreshRequest", _p, SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.class); - } - -} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java deleted file mode 100644 index 77ce6f7329..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java +++ /dev/null @@ -1,34 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; -import javax.annotation.processing.Generated; - -/** - * MCP headers refresh request id and the host response. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record SessionMcpHeadersHandlePendingHeadersRefreshRequestParams( - /** Target session identifier */ - @JsonProperty("sessionId") String sessionId, - /** Headers refresh request identifier from mcp.headers_refresh_required */ - @JsonProperty("requestId") String requestId, - /** Host response: supply dynamic headers or decline this refresh. */ - @JsonProperty("result") Object result -) { -} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java deleted file mode 100644 index a890713060..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; -import javax.annotation.processing.Generated; - -/** - * Indicates whether the pending MCP headers refresh response was accepted. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record SessionMcpHeadersHandlePendingHeadersRefreshRequestResult( - /** Whether the response was accepted. False if the request was unknown, timed out, or already resolved. */ - @JsonProperty("success") Boolean success -) { -} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java index dac9741a4d..4479a3175a 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java @@ -70,8 +70,6 @@ public record SessionOptionsUpdateParams( @JsonProperty("logInteractiveShells") Boolean logInteractiveShells, /** How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). */ @JsonProperty("envValueMode") OptionsUpdateEnvValueMode envValueMode, - /** Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. */ - @JsonProperty("allowAllMcpServerInstructions") Boolean allowAllMcpServerInstructions, /** Additional directories to search for skills. */ @JsonProperty("skillDirectories") List skillDirectories, /** Skill IDs that should be excluded from this session. */ @@ -129,8 +127,6 @@ public record SessionOptionsUpdateParams( /** Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. */ @JsonProperty("enableSkills") Boolean enableSkills, /** Context tier for models with tiered pricing. The session uses this to derive effective `modelCapabilitiesOverrides` so compaction, truncation, token display, and request limits honor the selected tier. */ - @JsonProperty("contextTier") OptionsUpdateContextTier contextTier, - /** Optional experimental response budget limits. Pass null to clear the response budget. */ - @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget + @JsonProperty("contextTier") OptionsUpdateContextTier contextTier ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java index d8c0c64fd0..44758350bd 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java @@ -39,11 +39,7 @@ public record SessionToolsUpdateSubagentSettingsParamsSubagents( /** Per-agent settings keyed by subagent agent_type */ @JsonProperty("agents") Map agents, /** Names of subagents the user has turned off; they cannot be dispatched */ - @JsonProperty("disabledSubagents") List disabledSubagents, - /** Maximum number of subagents that can run concurrently; applies to usage-based billing users only */ - @JsonProperty("maxConcurrency") Long maxConcurrency, - /** Maximum subagent nesting depth; applies to usage-based billing users only */ - @JsonProperty("maxDepth") Long maxDepth + @JsonProperty("disabledSubagents") List disabledSubagents ) { } } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java index 336c0eda84..265f24e669 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). + * Result of invoking the slash command (text output, prompt to send to the agent, or completion). * * @since 1.0.0 */ From eb50a5dc4cbee796ca1870b792ccebfd3a767997 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 26 Jun 2026 17:51:17 +0200 Subject: [PATCH 3/4] Restore Java generated MCP OAuth types The java-codegen-check auto-commit dropped generated event/RPC types that the MCP OAuth SDK code still references. Restore them and keep the static client secret event field so the Java SDK compiles and the MCP OAuth E2Es pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../copilot/generated/AssistantIdleEvent.java | 41 ++++++++++++++++ .../McpHeadersRefreshCompletedEvent.java | 43 ++++++++++++++++ .../McpHeadersRefreshCompletedOutcome.java | 37 ++++++++++++++ .../McpHeadersRefreshRequiredEvent.java | 47 ++++++++++++++++++ .../McpHeadersRefreshRequiredReason.java | 37 ++++++++++++++ .../generated/McpOauthRequestReason.java | 39 +++++++++++++++ .../generated/McpOauthRequiredEvent.java | 4 +- .../McpOauthRequiredStaticClientConfig.java | 2 + .../McpOauthWWWAuthenticateParams.java | 2 +- .../generated/ResponseBudgetConfig.java | 29 +++++++++++ .../copilot/generated/SessionEvent.java | 6 +++ .../copilot/generated/SessionResumeEvent.java | 2 + .../copilot/generated/SessionStartEvent.java | 2 + .../generated/ToolExecutionStartEvent.java | 2 + .../ToolExecutionStartShellToolInfo.java | 30 ++++++++++++ .../generated/UserMessageDelivery.java | 37 ++++++++++++++ .../copilot/generated/UserMessageEvent.java | 2 + .../copilot/generated/rpc/ModelBilling.java | 4 +- .../generated/rpc/ResponseBudgetConfig.java | 29 +++++++++++ .../copilot/generated/rpc/SessionMcpApi.java | 3 ++ .../generated/rpc/SessionMcpHeadersApi.java | 49 +++++++++++++++++++ ...dlePendingHeadersRefreshRequestParams.java | 34 +++++++++++++ ...dlePendingHeadersRefreshRequestResult.java | 30 ++++++++++++ .../rpc/SessionOptionsUpdateParams.java | 6 ++- ...sionToolsUpdateSubagentSettingsParams.java | 6 ++- .../rpc/SlashCommandInvocationResult.java | 2 +- 26 files changed, 519 insertions(+), 6 deletions(-) create mode 100644 java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java create mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java create mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java create mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java create mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java create mode 100644 java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java create mode 100644 java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java create mode 100644 java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java create mode 100644 java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java create mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java create mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java create mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java create mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java diff --git a/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java b/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java new file mode 100644 index 0000000000..3b79b8d50e --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "assistant.idle". Payload emitted whenever the main agent's processing loop goes idle, including while related background work (running agents or in-flight attached shell commands) is still pending and the session-level idle event is therefore deferred + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class AssistantIdleEvent extends SessionEvent { + + @Override + public String getType() { return "assistant.idle"; } + + @JsonProperty("data") + private AssistantIdleEventData data; + + public AssistantIdleEventData getData() { return data; } + public void setData(AssistantIdleEventData data) { this.data = data; } + + /** Data payload for {@link AssistantIdleEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record AssistantIdleEventData( + /** True when the preceding agentic loop was cancelled via abort signal */ + @JsonProperty("aborted") Boolean aborted + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java new file mode 100644 index 0000000000..a3ba903aea --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "mcp.headers_refresh_completed". MCP headers refresh request completion notification + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpHeadersRefreshCompletedEvent extends SessionEvent { + + @Override + public String getType() { return "mcp.headers_refresh_completed"; } + + @JsonProperty("data") + private McpHeadersRefreshCompletedEventData data; + + public McpHeadersRefreshCompletedEventData getData() { return data; } + public void setData(McpHeadersRefreshCompletedEventData data) { this.data = data; } + + /** Data payload for {@link McpHeadersRefreshCompletedEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record McpHeadersRefreshCompletedEventData( + /** Request ID of the resolved headers refresh request */ + @JsonProperty("requestId") String requestId, + /** How the pending MCP headers refresh request resolved. */ + @JsonProperty("outcome") McpHeadersRefreshCompletedOutcome outcome + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java new file mode 100644 index 0000000000..7980dd0a67 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * How the pending MCP headers refresh request resolved. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpHeadersRefreshCompletedOutcome { + /** The {@code headers} variant. */ + HEADERS("headers"), + /** The {@code none} variant. */ + NONE("none"), + /** The {@code timeout} variant. */ + TIMEOUT("timeout"); + + private final String value; + McpHeadersRefreshCompletedOutcome(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpHeadersRefreshCompletedOutcome fromValue(String value) { + for (McpHeadersRefreshCompletedOutcome v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpHeadersRefreshCompletedOutcome value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java new file mode 100644 index 0000000000..d8774bb326 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Session event "mcp.headers_refresh_required". Dynamic headers refresh request for a remote MCP server + * @since 1.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class McpHeadersRefreshRequiredEvent extends SessionEvent { + + @Override + public String getType() { return "mcp.headers_refresh_required"; } + + @JsonProperty("data") + private McpHeadersRefreshRequiredEventData data; + + public McpHeadersRefreshRequiredEventData getData() { return data; } + public void setData(McpHeadersRefreshRequiredEventData data) { this.data = data; } + + /** Data payload for {@link McpHeadersRefreshRequiredEvent}. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public record McpHeadersRefreshRequiredEventData( + /** Unique identifier for this headers refresh request; used to respond via session.mcp.headers.handlePendingHeadersRefreshRequest() */ + @JsonProperty("requestId") String requestId, + /** Display name of the remote MCP server requesting headers */ + @JsonProperty("serverName") String serverName, + /** URL of the remote MCP server requesting headers */ + @JsonProperty("serverUrl") String serverUrl, + /** Why dynamic headers are being requested. */ + @JsonProperty("reason") McpHeadersRefreshRequiredReason reason + ) { + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java new file mode 100644 index 0000000000..86c8f8b2d6 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Why dynamic headers are being requested. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpHeadersRefreshRequiredReason { + /** The {@code startup} variant. */ + STARTUP("startup"), + /** The {@code ttl-expired} variant. */ + TTL_EXPIRED("ttl-expired"), + /** The {@code auth-failed} variant. */ + AUTH_FAILED("auth-failed"); + + private final String value; + McpHeadersRefreshRequiredReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpHeadersRefreshRequiredReason fromValue(String value) { + for (McpHeadersRefreshRequiredReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpHeadersRefreshRequiredReason value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java new file mode 100644 index 0000000000..2a6eec7063 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * Reason the runtime is requesting host-provided MCP OAuth credentials + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum McpOauthRequestReason { + /** The {@code initial} variant. */ + INITIAL("initial"), + /** The {@code refresh} variant. */ + REFRESH("refresh"), + /** The {@code reauth} variant. */ + REAUTH("reauth"), + /** The {@code upscope} variant. */ + UPSCOPE("upscope"); + + private final String value; + McpOauthRequestReason(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static McpOauthRequestReason fromValue(String value) { + for (McpOauthRequestReason v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown McpOauthRequestReason value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java index 3f6ef8ef60..67413d382f 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java @@ -45,7 +45,9 @@ public record McpOauthRequiredEventData( /** OAuth WWW-Authenticate parameters parsed from the auth challenge, if available */ @JsonProperty("wwwAuthenticateParams") McpOauthWWWAuthenticateParams wwwAuthenticateParams, /** Raw OAuth protected-resource metadata document fetched for the MCP server, if available */ - @JsonProperty("resourceMetadata") String resourceMetadata + @JsonProperty("resourceMetadata") String resourceMetadata, + /** Why the runtime is requesting host-provided OAuth credentials. */ + @JsonProperty("reason") McpOauthRequestReason reason ) { } } diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java index 764f8b7fc8..5f42ec90c4 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java @@ -23,6 +23,8 @@ public record McpOauthRequiredStaticClientConfig( /** OAuth client ID for the server */ @JsonProperty("clientId") String clientId, + /** Optional OAuth client secret for confidential static clients, when the runtime can resolve one */ + @JsonProperty("clientSecret") String clientSecret, /** Whether this is a public OAuth client */ @JsonProperty("publicClient") Boolean publicClient, /** Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). */ diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java b/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java index faa08e1e89..3e1fdb0d10 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record McpOauthWWWAuthenticateParams( - /** Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter */ + /** Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter, if present */ @JsonProperty("resourceMetadataUrl") String resourceMetadataUrl, /** Requested OAuth scopes from the WWW-Authenticate scope parameter, if present */ @JsonProperty("scope") String scope, diff --git a/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java b/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java new file mode 100644 index 0000000000..0bc290e2df --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional experimental response budget limits. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ResponseBudgetConfig( + /** Maximum model-call iterations allowed while responding to one top-level user message. */ + @JsonProperty("maxModelIterations") Long maxModelIterations, + /** Maximum AI Credits allowed while responding to one top-level user message. */ + @JsonProperty("maxAiCredits") Double maxAiCredits +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java index f92dbf2fae..bcbb09831c 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -63,6 +63,7 @@ @JsonSubTypes.Type(value = AssistantMessageStartEvent.class, name = "assistant.message_start"), @JsonSubTypes.Type(value = AssistantMessageDeltaEvent.class, name = "assistant.message_delta"), @JsonSubTypes.Type(value = AssistantTurnEndEvent.class, name = "assistant.turn_end"), + @JsonSubTypes.Type(value = AssistantIdleEvent.class, name = "assistant.idle"), @JsonSubTypes.Type(value = AssistantUsageEvent.class, name = "assistant.usage"), @JsonSubTypes.Type(value = ModelCallFailureEvent.class, name = "model.call_failure"), @JsonSubTypes.Type(value = AbortEvent.class, name = "abort"), @@ -93,6 +94,8 @@ @JsonSubTypes.Type(value = SamplingCompletedEvent.class, name = "sampling.completed"), @JsonSubTypes.Type(value = McpOauthRequiredEvent.class, name = "mcp.oauth_required"), @JsonSubTypes.Type(value = McpOauthCompletedEvent.class, name = "mcp.oauth_completed"), + @JsonSubTypes.Type(value = McpHeadersRefreshRequiredEvent.class, name = "mcp.headers_refresh_required"), + @JsonSubTypes.Type(value = McpHeadersRefreshCompletedEvent.class, name = "mcp.headers_refresh_completed"), @JsonSubTypes.Type(value = SessionCustomNotificationEvent.class, name = "session.custom_notification"), @JsonSubTypes.Type(value = ExternalToolRequestedEvent.class, name = "external_tool.requested"), @JsonSubTypes.Type(value = ExternalToolCompletedEvent.class, name = "external_tool.completed"), @@ -161,6 +164,7 @@ public abstract sealed class SessionEvent permits AssistantMessageStartEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, + AssistantIdleEvent, AssistantUsageEvent, ModelCallFailureEvent, AbortEvent, @@ -191,6 +195,8 @@ public abstract sealed class SessionEvent permits SamplingCompletedEvent, McpOauthRequiredEvent, McpOauthCompletedEvent, + McpHeadersRefreshRequiredEvent, + McpHeadersRefreshCompletedEvent, SessionCustomNotificationEvent, ExternalToolRequestedEvent, ExternalToolCompletedEvent, diff --git a/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java index b1cfda9338..d4c2978777 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java @@ -49,6 +49,8 @@ public record SessionResumeEventData( @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Context tier currently selected at resume time; null when no tier is active */ @JsonProperty("contextTier") ContextTier contextTier, + /** Experimental response budget limits currently configured at resume time; null when no budget is active */ + @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget, /** Updated working directory and git context at resume time */ @JsonProperty("context") WorkingDirectoryContext context, /** Whether the session was already in use by another client at resume time */ diff --git a/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java index 2b36fa3e7b..6d2921ff05 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java @@ -53,6 +53,8 @@ public record SessionStartEventData( @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) */ @JsonProperty("contextTier") ContextTier contextTier, + /** Experimental response budget limits configured at session creation time, if any */ + @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget, /** Working directory and git context at session start */ @JsonProperty("context") WorkingDirectoryContext context, /** Whether the session was already in use by another client at start time */ diff --git a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java index cd4e5a137d..b119a3eb0e 100644 --- a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java @@ -40,6 +40,8 @@ public record ToolExecutionStartEventData( @JsonProperty("toolName") String toolName, /** Arguments passed to the tool */ @JsonProperty("arguments") Object arguments, + /** Shell-tool path hints derived from the command at start time for shell tools (bash/powershell/local_shell). Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. Absent for non-shell tools. */ + @JsonProperty("shellToolInfo") ToolExecutionStartShellToolInfo shellToolInfo, /** Model identifier that generated this tool call */ @JsonProperty("model") String model, /** Name of the MCP server hosting this tool, when the tool is an MCP tool */ diff --git a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java new file mode 100644 index 0000000000..88ac787fca --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import javax.annotation.processing.Generated; + +/** + * Shell-aware path hints for a shell tool's command, captured at start time so consumers can snapshot a file's pre-image before the tool runs. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ToolExecutionStartShellToolInfo( + /** File paths the command may read or write, derived from the command at start time. Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. */ + @JsonProperty("possiblePaths") List possiblePaths, + /** Whether the command includes a file write redirection (e.g., > or >>). */ + @JsonProperty("hasWriteFileRedirection") Boolean hasWriteFileRedirection +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java b/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java new file mode 100644 index 0000000000..ab64a88592 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: session-events.schema.json + +package com.github.copilot.generated; + +import javax.annotation.processing.Generated; + +/** + * How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public enum UserMessageDelivery { + /** The {@code idle} variant. */ + IDLE("idle"), + /** The {@code steering} variant. */ + STEERING("steering"), + /** The {@code queued} variant. */ + QUEUED("queued"); + + private final String value; + UserMessageDelivery(String value) { this.value = value; } + @com.fasterxml.jackson.annotation.JsonValue + public String getValue() { return value; } + @com.fasterxml.jackson.annotation.JsonCreator + public static UserMessageDelivery fromValue(String value) { + for (UserMessageDelivery v : values()) { + if (v.value.equals(value)) return v; + } + throw new IllegalArgumentException("Unknown UserMessageDelivery value: " + value); + } +} diff --git a/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java b/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java index 69f959ab65..57f64b6527 100644 --- a/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java @@ -47,6 +47,8 @@ public record UserMessageEventData( @JsonProperty("nativeDocumentPathFallbackPaths") List nativeDocumentPathFallbackPaths, /** Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user) */ @JsonProperty("source") String source, + /** How this message was delivered to the agentic loop relative to loop state (idle-start vs. steering/queued while busy). The timing axis; combine with `source` (origin) for the full picture. Used for telemetry attribution. */ + @JsonProperty("delivery") UserMessageDelivery delivery, /** The agent mode that was active when this message was sent */ @JsonProperty("agentMode") UserMessageAgentMode agentMode, /** True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. */ diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java index 94a8188f16..53ed64f4bf 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java @@ -24,6 +24,8 @@ public record ModelBilling( /** Billing cost multiplier relative to the base rate */ @JsonProperty("multiplier") Double multiplier, /** Token-level pricing information for this model */ - @JsonProperty("tokenPrices") ModelBillingTokenPrices tokenPrices + @JsonProperty("tokenPrices") ModelBillingTokenPrices tokenPrices, + /** Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. */ + @JsonProperty("discountPercent") Long discountPercent ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java b/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java new file mode 100644 index 0000000000..4cbe89b4be --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.processing.Generated; + +/** + * Optional experimental response budget limits. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record ResponseBudgetConfig( + /** Maximum model-call iterations allowed while responding to one top-level user message. */ + @JsonProperty("maxModelIterations") Long maxModelIterations, + /** Maximum AI Credits allowed while responding to one top-level user message. */ + @JsonProperty("maxAiCredits") Double maxAiCredits +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java index 4246b11f82..f4603c249f 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java @@ -26,6 +26,8 @@ public final class SessionMcpApi { /** API methods for the {@code mcp.oauth} sub-namespace. */ public final SessionMcpOauthApi oauth; + /** API methods for the {@code mcp.headers} sub-namespace. */ + public final SessionMcpHeadersApi headers; /** API methods for the {@code mcp.apps} sub-namespace. */ public final SessionMcpAppsApi apps; @@ -34,6 +36,7 @@ public final class SessionMcpApi { this.caller = caller; this.sessionId = sessionId; this.oauth = new SessionMcpOauthApi(caller, sessionId); + this.headers = new SessionMcpHeadersApi(caller, sessionId); this.apps = new SessionMcpAppsApi(caller, sessionId); } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java new file mode 100644 index 0000000000..45679f83a4 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.github.copilot.CopilotExperimental; +import java.util.concurrent.CompletableFuture; +import javax.annotation.processing.Generated; + +/** + * API methods for the {@code mcp.headers} namespace. + * + * @since 1.0.0 + */ +@javax.annotation.processing.Generated("copilot-sdk-codegen") +public final class SessionMcpHeadersApi { + + private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; + + private final RpcCaller caller; + private final String sessionId; + + /** @param caller the RPC transport function */ + SessionMcpHeadersApi(RpcCaller caller, String sessionId) { + this.caller = caller; + this.sessionId = sessionId; + } + + /** + * MCP headers refresh request id and the host response. + *

+ * Note: the {@code sessionId} field in the params record is overridden + * by the session-scoped wrapper; any value provided is ignored. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ + @CopilotExperimental + public CompletableFuture handlePendingHeadersRefreshRequest(SessionMcpHeadersHandlePendingHeadersRefreshRequestParams params) { + com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); + _p.put("sessionId", this.sessionId); + return caller.invoke("session.mcp.headers.handlePendingHeadersRefreshRequest", _p, SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.class); + } + +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java new file mode 100644 index 0000000000..77ce6f7329 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * MCP headers refresh request id and the host response. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpHeadersHandlePendingHeadersRefreshRequestParams( + /** Target session identifier */ + @JsonProperty("sessionId") String sessionId, + /** Headers refresh request identifier from mcp.headers_refresh_required */ + @JsonProperty("requestId") String requestId, + /** Host response: supply dynamic headers or decline this refresh. */ + @JsonProperty("result") Object result +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java new file mode 100644 index 0000000000..a890713060 --- /dev/null +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from: api.schema.json + +package com.github.copilot.generated.rpc; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.copilot.CopilotExperimental; +import javax.annotation.processing.Generated; + +/** + * Indicates whether the pending MCP headers refresh response was accepted. + * + * @apiNote This method is experimental and may change in a future version. + * @since 1.0.0 + */ +@CopilotExperimental +@javax.annotation.processing.Generated("copilot-sdk-codegen") +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public record SessionMcpHeadersHandlePendingHeadersRefreshRequestResult( + /** Whether the response was accepted. False if the request was unknown, timed out, or already resolved. */ + @JsonProperty("success") Boolean success +) { +} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java index 4479a3175a..dac9741a4d 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java @@ -70,6 +70,8 @@ public record SessionOptionsUpdateParams( @JsonProperty("logInteractiveShells") Boolean logInteractiveShells, /** How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). */ @JsonProperty("envValueMode") OptionsUpdateEnvValueMode envValueMode, + /** Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. */ + @JsonProperty("allowAllMcpServerInstructions") Boolean allowAllMcpServerInstructions, /** Additional directories to search for skills. */ @JsonProperty("skillDirectories") List skillDirectories, /** Skill IDs that should be excluded from this session. */ @@ -127,6 +129,8 @@ public record SessionOptionsUpdateParams( /** Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. */ @JsonProperty("enableSkills") Boolean enableSkills, /** Context tier for models with tiered pricing. The session uses this to derive effective `modelCapabilitiesOverrides` so compaction, truncation, token display, and request limits honor the selected tier. */ - @JsonProperty("contextTier") OptionsUpdateContextTier contextTier + @JsonProperty("contextTier") OptionsUpdateContextTier contextTier, + /** Optional experimental response budget limits. Pass null to clear the response budget. */ + @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java index 44758350bd..d8c0c64fd0 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java @@ -39,7 +39,11 @@ public record SessionToolsUpdateSubagentSettingsParamsSubagents( /** Per-agent settings keyed by subagent agent_type */ @JsonProperty("agents") Map agents, /** Names of subagents the user has turned off; they cannot be dispatched */ - @JsonProperty("disabledSubagents") List disabledSubagents + @JsonProperty("disabledSubagents") List disabledSubagents, + /** Maximum number of subagents that can run concurrently; applies to usage-based billing users only */ + @JsonProperty("maxConcurrency") Long maxConcurrency, + /** Maximum subagent nesting depth; applies to usage-based billing users only */ + @JsonProperty("maxDepth") Long maxDepth ) { } } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java index 265f24e669..336c0eda84 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Result of invoking the slash command (text output, prompt to send to the agent, or completion). + * Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). * * @since 1.0.0 */ From 1ef3726c92f48f109c04a68979944f8e26efb1f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:51:40 +0000 Subject: [PATCH 4/4] Regenerate Java codegen output Auto-committed by java-codegen-check workflow. --- .../copilot/generated/AssistantIdleEvent.java | 41 ---------------- .../McpHeadersRefreshCompletedEvent.java | 43 ---------------- .../McpHeadersRefreshCompletedOutcome.java | 37 -------------- .../McpHeadersRefreshRequiredEvent.java | 47 ------------------ .../McpHeadersRefreshRequiredReason.java | 37 -------------- .../generated/McpOauthRequestReason.java | 39 --------------- .../generated/McpOauthRequiredEvent.java | 4 +- .../McpOauthRequiredStaticClientConfig.java | 2 - .../McpOauthWWWAuthenticateParams.java | 2 +- .../generated/ResponseBudgetConfig.java | 29 ----------- .../copilot/generated/SessionEvent.java | 6 --- .../copilot/generated/SessionResumeEvent.java | 2 - .../copilot/generated/SessionStartEvent.java | 2 - .../generated/ToolExecutionStartEvent.java | 2 - .../ToolExecutionStartShellToolInfo.java | 30 ------------ .../generated/UserMessageDelivery.java | 37 -------------- .../copilot/generated/UserMessageEvent.java | 2 - .../copilot/generated/rpc/ModelBilling.java | 4 +- .../generated/rpc/ResponseBudgetConfig.java | 29 ----------- .../copilot/generated/rpc/SessionMcpApi.java | 3 -- .../generated/rpc/SessionMcpHeadersApi.java | 49 ------------------- ...dlePendingHeadersRefreshRequestParams.java | 34 ------------- ...dlePendingHeadersRefreshRequestResult.java | 30 ------------ .../rpc/SessionOptionsUpdateParams.java | 6 +-- ...sionToolsUpdateSubagentSettingsParams.java | 6 +-- .../rpc/SlashCommandInvocationResult.java | 2 +- 26 files changed, 6 insertions(+), 519 deletions(-) delete mode 100644 java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java delete mode 100644 java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java diff --git a/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java b/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java deleted file mode 100644 index 3b79b8d50e..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/AssistantIdleEvent.java +++ /dev/null @@ -1,41 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Session event "assistant.idle". Payload emitted whenever the main agent's processing loop goes idle, including while related background work (running agents or in-flight attached shell commands) is still pending and the session-level idle event is therefore deferred - * @since 1.0.0 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class AssistantIdleEvent extends SessionEvent { - - @Override - public String getType() { return "assistant.idle"; } - - @JsonProperty("data") - private AssistantIdleEventData data; - - public AssistantIdleEventData getData() { return data; } - public void setData(AssistantIdleEventData data) { this.data = data; } - - /** Data payload for {@link AssistantIdleEvent}. */ - @JsonIgnoreProperties(ignoreUnknown = true) - @JsonInclude(JsonInclude.Include.NON_NULL) - public record AssistantIdleEventData( - /** True when the preceding agentic loop was cancelled via abort signal */ - @JsonProperty("aborted") Boolean aborted - ) { - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java deleted file mode 100644 index a3ba903aea..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedEvent.java +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Session event "mcp.headers_refresh_completed". MCP headers refresh request completion notification - * @since 1.0.0 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class McpHeadersRefreshCompletedEvent extends SessionEvent { - - @Override - public String getType() { return "mcp.headers_refresh_completed"; } - - @JsonProperty("data") - private McpHeadersRefreshCompletedEventData data; - - public McpHeadersRefreshCompletedEventData getData() { return data; } - public void setData(McpHeadersRefreshCompletedEventData data) { this.data = data; } - - /** Data payload for {@link McpHeadersRefreshCompletedEvent}. */ - @JsonIgnoreProperties(ignoreUnknown = true) - @JsonInclude(JsonInclude.Include.NON_NULL) - public record McpHeadersRefreshCompletedEventData( - /** Request ID of the resolved headers refresh request */ - @JsonProperty("requestId") String requestId, - /** How the pending MCP headers refresh request resolved. */ - @JsonProperty("outcome") McpHeadersRefreshCompletedOutcome outcome - ) { - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java deleted file mode 100644 index 7980dd0a67..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshCompletedOutcome.java +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import javax.annotation.processing.Generated; - -/** - * How the pending MCP headers refresh request resolved. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum McpHeadersRefreshCompletedOutcome { - /** The {@code headers} variant. */ - HEADERS("headers"), - /** The {@code none} variant. */ - NONE("none"), - /** The {@code timeout} variant. */ - TIMEOUT("timeout"); - - private final String value; - McpHeadersRefreshCompletedOutcome(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static McpHeadersRefreshCompletedOutcome fromValue(String value) { - for (McpHeadersRefreshCompletedOutcome v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown McpHeadersRefreshCompletedOutcome value: " + value); - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java deleted file mode 100644 index d8774bb326..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredEvent.java +++ /dev/null @@ -1,47 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Session event "mcp.headers_refresh_required". Dynamic headers refresh request for a remote MCP server - * @since 1.0.0 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class McpHeadersRefreshRequiredEvent extends SessionEvent { - - @Override - public String getType() { return "mcp.headers_refresh_required"; } - - @JsonProperty("data") - private McpHeadersRefreshRequiredEventData data; - - public McpHeadersRefreshRequiredEventData getData() { return data; } - public void setData(McpHeadersRefreshRequiredEventData data) { this.data = data; } - - /** Data payload for {@link McpHeadersRefreshRequiredEvent}. */ - @JsonIgnoreProperties(ignoreUnknown = true) - @JsonInclude(JsonInclude.Include.NON_NULL) - public record McpHeadersRefreshRequiredEventData( - /** Unique identifier for this headers refresh request; used to respond via session.mcp.headers.handlePendingHeadersRefreshRequest() */ - @JsonProperty("requestId") String requestId, - /** Display name of the remote MCP server requesting headers */ - @JsonProperty("serverName") String serverName, - /** URL of the remote MCP server requesting headers */ - @JsonProperty("serverUrl") String serverUrl, - /** Why dynamic headers are being requested. */ - @JsonProperty("reason") McpHeadersRefreshRequiredReason reason - ) { - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java b/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java deleted file mode 100644 index 86c8f8b2d6..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/McpHeadersRefreshRequiredReason.java +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import javax.annotation.processing.Generated; - -/** - * Why dynamic headers are being requested. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum McpHeadersRefreshRequiredReason { - /** The {@code startup} variant. */ - STARTUP("startup"), - /** The {@code ttl-expired} variant. */ - TTL_EXPIRED("ttl-expired"), - /** The {@code auth-failed} variant. */ - AUTH_FAILED("auth-failed"); - - private final String value; - McpHeadersRefreshRequiredReason(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static McpHeadersRefreshRequiredReason fromValue(String value) { - for (McpHeadersRefreshRequiredReason v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown McpHeadersRefreshRequiredReason value: " + value); - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java deleted file mode 100644 index 2a6eec7063..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthRequestReason.java +++ /dev/null @@ -1,39 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import javax.annotation.processing.Generated; - -/** - * Reason the runtime is requesting host-provided MCP OAuth credentials - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum McpOauthRequestReason { - /** The {@code initial} variant. */ - INITIAL("initial"), - /** The {@code refresh} variant. */ - REFRESH("refresh"), - /** The {@code reauth} variant. */ - REAUTH("reauth"), - /** The {@code upscope} variant. */ - UPSCOPE("upscope"); - - private final String value; - McpOauthRequestReason(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static McpOauthRequestReason fromValue(String value) { - for (McpOauthRequestReason v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown McpOauthRequestReason value: " + value); - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java index 67413d382f..3f6ef8ef60 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredEvent.java @@ -45,9 +45,7 @@ public record McpOauthRequiredEventData( /** OAuth WWW-Authenticate parameters parsed from the auth challenge, if available */ @JsonProperty("wwwAuthenticateParams") McpOauthWWWAuthenticateParams wwwAuthenticateParams, /** Raw OAuth protected-resource metadata document fetched for the MCP server, if available */ - @JsonProperty("resourceMetadata") String resourceMetadata, - /** Why the runtime is requesting host-provided OAuth credentials. */ - @JsonProperty("reason") McpOauthRequestReason reason + @JsonProperty("resourceMetadata") String resourceMetadata ) { } } diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java index 5f42ec90c4..764f8b7fc8 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthRequiredStaticClientConfig.java @@ -23,8 +23,6 @@ public record McpOauthRequiredStaticClientConfig( /** OAuth client ID for the server */ @JsonProperty("clientId") String clientId, - /** Optional OAuth client secret for confidential static clients, when the runtime can resolve one */ - @JsonProperty("clientSecret") String clientSecret, /** Whether this is a public OAuth client */ @JsonProperty("publicClient") Boolean publicClient, /** Optional non-default OAuth grant type. When set to 'client_credentials', the OAuth flow runs headlessly using the client_id + keychain-stored secret (no browser, no callback server). */ diff --git a/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java b/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java index 3e1fdb0d10..faa08e1e89 100644 --- a/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java +++ b/java/src/generated/java/com/github/copilot/generated/McpOauthWWWAuthenticateParams.java @@ -21,7 +21,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public record McpOauthWWWAuthenticateParams( - /** Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter, if present */ + /** Protected resource metadata URL from the WWW-Authenticate resource_metadata parameter */ @JsonProperty("resourceMetadataUrl") String resourceMetadataUrl, /** Requested OAuth scopes from the WWW-Authenticate scope parameter, if present */ @JsonProperty("scope") String scope, diff --git a/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java b/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java deleted file mode 100644 index 0bc290e2df..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/ResponseBudgetConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Optional experimental response budget limits. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record ResponseBudgetConfig( - /** Maximum model-call iterations allowed while responding to one top-level user message. */ - @JsonProperty("maxModelIterations") Long maxModelIterations, - /** Maximum AI Credits allowed while responding to one top-level user message. */ - @JsonProperty("maxAiCredits") Double maxAiCredits -) { -} diff --git a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java index bcbb09831c..f92dbf2fae 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -63,7 +63,6 @@ @JsonSubTypes.Type(value = AssistantMessageStartEvent.class, name = "assistant.message_start"), @JsonSubTypes.Type(value = AssistantMessageDeltaEvent.class, name = "assistant.message_delta"), @JsonSubTypes.Type(value = AssistantTurnEndEvent.class, name = "assistant.turn_end"), - @JsonSubTypes.Type(value = AssistantIdleEvent.class, name = "assistant.idle"), @JsonSubTypes.Type(value = AssistantUsageEvent.class, name = "assistant.usage"), @JsonSubTypes.Type(value = ModelCallFailureEvent.class, name = "model.call_failure"), @JsonSubTypes.Type(value = AbortEvent.class, name = "abort"), @@ -94,8 +93,6 @@ @JsonSubTypes.Type(value = SamplingCompletedEvent.class, name = "sampling.completed"), @JsonSubTypes.Type(value = McpOauthRequiredEvent.class, name = "mcp.oauth_required"), @JsonSubTypes.Type(value = McpOauthCompletedEvent.class, name = "mcp.oauth_completed"), - @JsonSubTypes.Type(value = McpHeadersRefreshRequiredEvent.class, name = "mcp.headers_refresh_required"), - @JsonSubTypes.Type(value = McpHeadersRefreshCompletedEvent.class, name = "mcp.headers_refresh_completed"), @JsonSubTypes.Type(value = SessionCustomNotificationEvent.class, name = "session.custom_notification"), @JsonSubTypes.Type(value = ExternalToolRequestedEvent.class, name = "external_tool.requested"), @JsonSubTypes.Type(value = ExternalToolCompletedEvent.class, name = "external_tool.completed"), @@ -164,7 +161,6 @@ public abstract sealed class SessionEvent permits AssistantMessageStartEvent, AssistantMessageDeltaEvent, AssistantTurnEndEvent, - AssistantIdleEvent, AssistantUsageEvent, ModelCallFailureEvent, AbortEvent, @@ -195,8 +191,6 @@ public abstract sealed class SessionEvent permits SamplingCompletedEvent, McpOauthRequiredEvent, McpOauthCompletedEvent, - McpHeadersRefreshRequiredEvent, - McpHeadersRefreshCompletedEvent, SessionCustomNotificationEvent, ExternalToolRequestedEvent, ExternalToolCompletedEvent, diff --git a/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java index d4c2978777..b1cfda9338 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionResumeEvent.java @@ -49,8 +49,6 @@ public record SessionResumeEventData( @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Context tier currently selected at resume time; null when no tier is active */ @JsonProperty("contextTier") ContextTier contextTier, - /** Experimental response budget limits currently configured at resume time; null when no budget is active */ - @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget, /** Updated working directory and git context at resume time */ @JsonProperty("context") WorkingDirectoryContext context, /** Whether the session was already in use by another client at resume time */ diff --git a/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java index 6d2921ff05..2b36fa3e7b 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionStartEvent.java @@ -53,8 +53,6 @@ public record SessionStartEventData( @JsonProperty("reasoningSummary") ReasoningSummary reasoningSummary, /** Context tier selected at session creation time for models with tiered context pricing; null when no tier is selected (e.g., non-tiered model) */ @JsonProperty("contextTier") ContextTier contextTier, - /** Experimental response budget limits configured at session creation time, if any */ - @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget, /** Working directory and git context at session start */ @JsonProperty("context") WorkingDirectoryContext context, /** Whether the session was already in use by another client at start time */ diff --git a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java index b119a3eb0e..cd4e5a137d 100644 --- a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartEvent.java @@ -40,8 +40,6 @@ public record ToolExecutionStartEventData( @JsonProperty("toolName") String toolName, /** Arguments passed to the tool */ @JsonProperty("arguments") Object arguments, - /** Shell-tool path hints derived from the command at start time for shell tools (bash/powershell/local_shell). Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. Absent for non-shell tools. */ - @JsonProperty("shellToolInfo") ToolExecutionStartShellToolInfo shellToolInfo, /** Model identifier that generated this tool call */ @JsonProperty("model") String model, /** Name of the MCP server hosting this tool, when the tool is an MCP tool */ diff --git a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java b/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java deleted file mode 100644 index 88ac787fca..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/ToolExecutionStartShellToolInfo.java +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.List; -import javax.annotation.processing.Generated; - -/** - * Shell-aware path hints for a shell tool's command, captured at start time so consumers can snapshot a file's pre-image before the tool runs. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record ToolExecutionStartShellToolInfo( - /** File paths the command may read or write, derived from the command at start time. Produced by the same shell-aware extractor as PermissionRequestShell.possiblePaths, so it is present even when the command is auto-approved and no permission request fires. */ - @JsonProperty("possiblePaths") List possiblePaths, - /** Whether the command includes a file write redirection (e.g., > or >>). */ - @JsonProperty("hasWriteFileRedirection") Boolean hasWriteFileRedirection -) { -} diff --git a/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java b/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java deleted file mode 100644 index ab64a88592..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/UserMessageDelivery.java +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: session-events.schema.json - -package com.github.copilot.generated; - -import javax.annotation.processing.Generated; - -/** - * How this user message was delivered to the agentic loop, relative to whether the loop was already running. This is the timing axis only; the message's origin (human vs. system/command/schedule/skill/etc.) is carried separately by `source`. A system-injected message has a delivery too — e.g. a background-task notification waking an idle agent is `idle`, the same mechanism as a human starting a fresh turn. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public enum UserMessageDelivery { - /** The {@code idle} variant. */ - IDLE("idle"), - /** The {@code steering} variant. */ - STEERING("steering"), - /** The {@code queued} variant. */ - QUEUED("queued"); - - private final String value; - UserMessageDelivery(String value) { this.value = value; } - @com.fasterxml.jackson.annotation.JsonValue - public String getValue() { return value; } - @com.fasterxml.jackson.annotation.JsonCreator - public static UserMessageDelivery fromValue(String value) { - for (UserMessageDelivery v : values()) { - if (v.value.equals(value)) return v; - } - throw new IllegalArgumentException("Unknown UserMessageDelivery value: " + value); - } -} diff --git a/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java b/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java index 57f64b6527..69f959ab65 100644 --- a/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/UserMessageEvent.java @@ -47,8 +47,6 @@ public record UserMessageEventData( @JsonProperty("nativeDocumentPathFallbackPaths") List nativeDocumentPathFallbackPaths, /** Origin of this message, used for timeline filtering (e.g., "skill-pdf" for skill-injected messages that should be hidden from the user) */ @JsonProperty("source") String source, - /** How this message was delivered to the agentic loop relative to loop state (idle-start vs. steering/queued while busy). The timing axis; combine with `source` (origin) for the full picture. Used for telemetry attribution. */ - @JsonProperty("delivery") UserMessageDelivery delivery, /** The agent mode that was active when this message was sent */ @JsonProperty("agentMode") UserMessageAgentMode agentMode, /** True when this user message was auto-injected by autopilot's continuation loop rather than typed by the user; used to distinguish autopilot-driven turns in telemetry. */ diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java index 53ed64f4bf..94a8188f16 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/ModelBilling.java @@ -24,8 +24,6 @@ public record ModelBilling( /** Billing cost multiplier relative to the base rate */ @JsonProperty("multiplier") Double multiplier, /** Token-level pricing information for this model */ - @JsonProperty("tokenPrices") ModelBillingTokenPrices tokenPrices, - /** Whole-number percentage discount (0-100) applied to usage billed through this model. Populated for the synthetic `auto` model, where requests routed by auto-mode are billed at a reduced rate; absent for concrete models. */ - @JsonProperty("discountPercent") Long discountPercent + @JsonProperty("tokenPrices") ModelBillingTokenPrices tokenPrices ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java b/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java deleted file mode 100644 index 4cbe89b4be..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/rpc/ResponseBudgetConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import javax.annotation.processing.Generated; - -/** - * Optional experimental response budget limits. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record ResponseBudgetConfig( - /** Maximum model-call iterations allowed while responding to one top-level user message. */ - @JsonProperty("maxModelIterations") Long maxModelIterations, - /** Maximum AI Credits allowed while responding to one top-level user message. */ - @JsonProperty("maxAiCredits") Double maxAiCredits -) { -} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java index f4603c249f..4246b11f82 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpApi.java @@ -26,8 +26,6 @@ public final class SessionMcpApi { /** API methods for the {@code mcp.oauth} sub-namespace. */ public final SessionMcpOauthApi oauth; - /** API methods for the {@code mcp.headers} sub-namespace. */ - public final SessionMcpHeadersApi headers; /** API methods for the {@code mcp.apps} sub-namespace. */ public final SessionMcpAppsApi apps; @@ -36,7 +34,6 @@ public final class SessionMcpApi { this.caller = caller; this.sessionId = sessionId; this.oauth = new SessionMcpOauthApi(caller, sessionId); - this.headers = new SessionMcpHeadersApi(caller, sessionId); this.apps = new SessionMcpAppsApi(caller, sessionId); } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java deleted file mode 100644 index 45679f83a4..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersApi.java +++ /dev/null @@ -1,49 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.github.copilot.CopilotExperimental; -import java.util.concurrent.CompletableFuture; -import javax.annotation.processing.Generated; - -/** - * API methods for the {@code mcp.headers} namespace. - * - * @since 1.0.0 - */ -@javax.annotation.processing.Generated("copilot-sdk-codegen") -public final class SessionMcpHeadersApi { - - private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = RpcMapper.INSTANCE; - - private final RpcCaller caller; - private final String sessionId; - - /** @param caller the RPC transport function */ - SessionMcpHeadersApi(RpcCaller caller, String sessionId) { - this.caller = caller; - this.sessionId = sessionId; - } - - /** - * MCP headers refresh request id and the host response. - *

- * Note: the {@code sessionId} field in the params record is overridden - * by the session-scoped wrapper; any value provided is ignored. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ - @CopilotExperimental - public CompletableFuture handlePendingHeadersRefreshRequest(SessionMcpHeadersHandlePendingHeadersRefreshRequestParams params) { - com.fasterxml.jackson.databind.node.ObjectNode _p = MAPPER.valueToTree(params); - _p.put("sessionId", this.sessionId); - return caller.invoke("session.mcp.headers.handlePendingHeadersRefreshRequest", _p, SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.class); - } - -} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java deleted file mode 100644 index 77ce6f7329..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestParams.java +++ /dev/null @@ -1,34 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; -import javax.annotation.processing.Generated; - -/** - * MCP headers refresh request id and the host response. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record SessionMcpHeadersHandlePendingHeadersRefreshRequestParams( - /** Target session identifier */ - @JsonProperty("sessionId") String sessionId, - /** Headers refresh request identifier from mcp.headers_refresh_required */ - @JsonProperty("requestId") String requestId, - /** Host response: supply dynamic headers or decline this refresh. */ - @JsonProperty("result") Object result -) { -} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java deleted file mode 100644 index a890713060..0000000000 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionMcpHeadersHandlePendingHeadersRefreshRequestResult.java +++ /dev/null @@ -1,30 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------------------------------------------*/ - -// AUTO-GENERATED FILE - DO NOT EDIT -// Generated from: api.schema.json - -package com.github.copilot.generated.rpc; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.github.copilot.CopilotExperimental; -import javax.annotation.processing.Generated; - -/** - * Indicates whether the pending MCP headers refresh response was accepted. - * - * @apiNote This method is experimental and may change in a future version. - * @since 1.0.0 - */ -@CopilotExperimental -@javax.annotation.processing.Generated("copilot-sdk-codegen") -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public record SessionMcpHeadersHandlePendingHeadersRefreshRequestResult( - /** Whether the response was accepted. False if the request was unknown, timed out, or already resolved. */ - @JsonProperty("success") Boolean success -) { -} diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java index dac9741a4d..4479a3175a 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionOptionsUpdateParams.java @@ -70,8 +70,6 @@ public record SessionOptionsUpdateParams( @JsonProperty("logInteractiveShells") Boolean logInteractiveShells, /** How env values are passed to MCP servers (`direct` inlines literal values; `indirect` resolves at launch). */ @JsonProperty("envValueMode") OptionsUpdateEnvValueMode envValueMode, - /** Whether to include instructions from every MCP server in the system prompt instead of only allowlisted servers. */ - @JsonProperty("allowAllMcpServerInstructions") Boolean allowAllMcpServerInstructions, /** Additional directories to search for skills. */ @JsonProperty("skillDirectories") List skillDirectories, /** Skill IDs that should be excluded from this session. */ @@ -129,8 +127,6 @@ public record SessionOptionsUpdateParams( /** Whether to enable skill directory scanning and loading. Falls back to enableConfigDiscovery when unset. */ @JsonProperty("enableSkills") Boolean enableSkills, /** Context tier for models with tiered pricing. The session uses this to derive effective `modelCapabilitiesOverrides` so compaction, truncation, token display, and request limits honor the selected tier. */ - @JsonProperty("contextTier") OptionsUpdateContextTier contextTier, - /** Optional experimental response budget limits. Pass null to clear the response budget. */ - @JsonProperty("responseBudget") ResponseBudgetConfig responseBudget + @JsonProperty("contextTier") OptionsUpdateContextTier contextTier ) { } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java b/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java index d8c0c64fd0..44758350bd 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SessionToolsUpdateSubagentSettingsParams.java @@ -39,11 +39,7 @@ public record SessionToolsUpdateSubagentSettingsParamsSubagents( /** Per-agent settings keyed by subagent agent_type */ @JsonProperty("agents") Map agents, /** Names of subagents the user has turned off; they cannot be dispatched */ - @JsonProperty("disabledSubagents") List disabledSubagents, - /** Maximum number of subagents that can run concurrently; applies to usage-based billing users only */ - @JsonProperty("maxConcurrency") Long maxConcurrency, - /** Maximum subagent nesting depth; applies to usage-based billing users only */ - @JsonProperty("maxDepth") Long maxDepth + @JsonProperty("disabledSubagents") List disabledSubagents ) { } } diff --git a/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java b/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java index 336c0eda84..265f24e669 100644 --- a/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java +++ b/java/src/generated/java/com/github/copilot/generated/rpc/SlashCommandInvocationResult.java @@ -13,7 +13,7 @@ import javax.annotation.processing.Generated; /** - * Result of invoking the slash command (text output, prompt to send to the agent, completion, or subcommand selection). + * Result of invoking the slash command (text output, prompt to send to the agent, or completion). * * @since 1.0.0 */